1/*******************************************************************
2* bugzillalib.cpp
3* Copyright 2009, 2011 Dario Andres Rodriguez <andresbajotierra@gmail.com>
4* Copyright 2012 George Kiagiadakis <kiagiadakis.george@gmail.com>
5*
6* This program is free software; you can redistribute it and/or
7* modify it under the terms of the GNU General Public License as
8* published by the Free Software Foundation; either version 2 of
9* the License, or (at your option) any later version.
10*
11* This program is distributed in the hope that it will be useful,
12* but WITHOUT ANY WARRANTY; without even the implied warranty of
13* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14* GNU General Public License for more details.
15*
16* You should have received a copy of the GNU General Public License
17* along with this program. If not, see <http://www.gnu.org/licenses/>.
18*
19******************************************************************/
20
21#include "bugzillalib.h"
22
23#include <QtCore/QTextStream>
24#include <QtCore/QByteArray>
25#include <QtCore/QString>
26
27#include <QtXml/QDomNode>
28#include <QtXml/QDomNodeList>
29#include <QtXml/QDomElement>
30#include <QtXml/QDomNamedNodeMap>
31
32#include <KIO/Job>
33#include <KUrl>
34#include <KLocalizedString>
35#include <KDebug>
36
37
38static const char columns[] = "bug_severity,priority,bug_status,product,short_desc,resolution";
39
40//Bugzilla URLs
41static const char searchUrl[] =
42 "buglist.cgi?query_format=advanced&order=Importance&ctype=csv"
43 "&product=%1"
44 "&longdesc_type=allwordssubstr&longdesc=%2"
45 "&chfieldfrom=%3&chfieldto=%4&chfield=[Bug+creation]"
46 "&bug_severity=%5"
47 "&columnlist=%6";
48// short_desc, product, long_desc(possible backtraces lines), searchFrom, searchTo, severity, columnList
49static const char showBugUrl[] = "show_bug.cgi?id=%1";
50static const char fetchBugUrl[] = "show_bug.cgi?id=%1&ctype=xml";
51
52static inline Component buildComponent(const QVariantMap& map);
53static inline Version buildVersion(const QVariantMap& map);
54static inline Product buildProduct(const QVariantMap& map);
55
56//BEGIN BugzillaManager
57
58BugzillaManager::BugzillaManager(const QString &bugTrackerUrl, QObject *parent)
59 : QObject(parent)
60 , m_bugTrackerUrl(bugTrackerUrl)
61 , m_logged(false)
62 , m_searchJob(0)
63{
64 m_xmlRpcClient = new KXmlRpc::Client(KUrl(m_bugTrackerUrl + "xmlrpc.cgi"), this);
65 m_xmlRpcClient->setUserAgent(QLatin1String("DrKonqi"));
66}
67
68//BEGIN Login methods
69void BugzillaManager::tryLogin(const QString& username, const QString& password)
70{
71 m_username = username;
72 m_logged = false;
73
74 QMap<QString, QVariant> args;
75 args.insert(QLatin1String("login"), username);
76 args.insert(QLatin1String("password"), password);
77 args.insert(QLatin1String("remember"), false);
78
79 m_xmlRpcClient->call(QLatin1String("User.login"), args,
80 this, SLOT(callMessage(QList<QVariant>,QVariant)),
81 this, SLOT(callFault(int,QString,QVariant)),
82 QString::fromAscii("login"));
83}
84
85bool BugzillaManager::getLogged() const
86{
87 return m_logged;
88}
89
90QString BugzillaManager::getUsername() const
91{
92 return m_username;
93}
94//END Login methods
95
96//BEGIN Bugzilla Action methods
97void BugzillaManager::fetchBugReport(int bugnumber, QObject * jobOwner)
98{
99 KUrl url = KUrl(QString(m_bugTrackerUrl) + QString(fetchBugUrl).arg(bugnumber));
100
101 if (!jobOwner) {
102 jobOwner = this;
103 }
104
105 KIO::Job * fetchBugJob = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo);
106 fetchBugJob->setParent(jobOwner);
107 connect(fetchBugJob, SIGNAL(finished(KJob*)) , this, SLOT(fetchBugJobFinished(KJob*)));
108}
109
110
111void BugzillaManager::searchBugs(const QStringList & products,
112 const QString & severity, const QString & date_start,
113 const QString & date_end, QString comment)
114{
115 QString product;
116 if (products.size() > 0) {
117 if (products.size() == 1) {
118 product = products.at(0);
119 } else {
120 Q_FOREACH(const QString & p, products) {
121 product += p + "&product=";
122 }
123 product = product.mid(0,product.size()-9);
124 }
125 }
126
127 QString url = QString(m_bugTrackerUrl) +
128 QString(searchUrl).arg(product, comment.replace(' ' , '+'), date_start,
129 date_end, severity, QString(columns));
130
131 stopCurrentSearch();
132
133 m_searchJob = KIO::storedGet(KUrl(url) , KIO::Reload, KIO::HideProgressInfo);
134 connect(m_searchJob, SIGNAL(finished(KJob*)) , this, SLOT(searchBugsJobFinished(KJob*)));
135}
136
137void BugzillaManager::sendReport(const BugReport & report)
138{
139 QMap<QString, QVariant> args;
140 args.insert(QLatin1String("product"), report.product());
141 args.insert(QLatin1String("component"), report.component());
142 args.insert(QLatin1String("version"), report.version());
143 args.insert(QLatin1String("summary"), report.shortDescription());
144 args.insert(QLatin1String("description"), report.description());
145 args.insert(QLatin1String("op_sys"), report.operatingSystem());
146 args.insert(QLatin1String("platform"), report.platform());
147 args.insert(QLatin1String("keywords"), report.keywords());
148 args.insert(QLatin1String("priority"), report.priority());
149 args.insert(QLatin1String("severity"), report.bugSeverity());
150
151 m_xmlRpcClient->call(QLatin1String("Bug.create"), args,
152 this, SLOT(callMessage(QList<QVariant>,QVariant)),
153 this, SLOT(callFault(int,QString,QVariant)),
154 QString::fromAscii("Bug.create"));
155}
156
157void BugzillaManager::attachTextToReport(const QString & text, const QString & filename,
158 const QString & summary, int bugId, const QString & comment)
159{
160 QMap<QString, QVariant> args;
161 args.insert(QLatin1String("ids"), QVariantList() << bugId);
162 args.insert(QLatin1String("file_name"), filename);
163 args.insert(QLatin1String("summary"), summary);
164 args.insert(QLatin1String("comment"), comment);
165 args.insert(QLatin1String("content_type"), QString::fromAscii("text/plain"));
166
167 //data needs to be a QByteArray so that it is encoded in base64 (query.cpp:246)
168 args.insert(QLatin1String("data"), text.toUtf8());
169
170 m_xmlRpcClient->call(QLatin1String("Bug.add_attachment"), args,
171 this, SLOT(callMessage(QList<QVariant>,QVariant)),
172 this, SLOT(callFault(int,QString,QVariant)),
173 QString::fromAscii("Bug.add_attachment"));
174}
175
176void BugzillaManager::addMeToCC(int bugId)
177{
178 QMap<QString, QVariant> args;
179 args.insert(QLatin1String("ids"), QVariantList() << bugId);
180
181 QMap<QString, QVariant> ccChanges;
182 ccChanges.insert(QLatin1String("add"), QVariantList() << m_username);
183 args.insert(QLatin1String("cc"), ccChanges);
184
185 m_xmlRpcClient->call(QLatin1String("Bug.update"), args,
186 this, SLOT(callMessage(QList<QVariant>,QVariant)),
187 this, SLOT(callFault(int,QString,QVariant)),
188 QString::fromAscii("Bug.update.cc"));
189}
190
191void BugzillaManager::fetchProductInfo(const QString & product)
192{
193 QMap<QString, QVariant> args;
194
195 args.insert("names", (QStringList() << product) ) ;
196
197 QStringList includeFields;
198 // currently we only need these informations
199 includeFields << "name" << "is_active" << "components" << "versions";
200
201 args.insert("include_fields", includeFields) ;
202
203 m_xmlRpcClient->call(QLatin1String("Product.get"), args,
204 this, SLOT(callMessage(QList<QVariant>,QVariant)),
205 this, SLOT(callFault(int,QString,QVariant)),
206 QString::fromAscii("Product.get.versions"));
207}
208
209
210//END Bugzilla Action methods
211
212//BEGIN Misc methods
213QString BugzillaManager::urlForBug(int bug_number) const
214{
215 return QString(m_bugTrackerUrl) + QString(showBugUrl).arg(bug_number);
216}
217
218void BugzillaManager::stopCurrentSearch()
219{
220 if (m_searchJob) { //Stop previous searchJob
221 m_searchJob->disconnect();
222 m_searchJob->kill();
223 m_searchJob = 0;
224 }
225}
226//END Misc methods
227
228//BEGIN Slots to handle KJob::finished
229
230void BugzillaManager::fetchBugJobFinished(KJob* job)
231{
232 if (!job->error()) {
233 KIO::StoredTransferJob * fetchBugJob = static_cast<KIO::StoredTransferJob*>(job);
234
235 BugReportXMLParser * parser = new BugReportXMLParser(fetchBugJob->data());
236 BugReport report = parser->parse();
237
238 if (parser->isValid()) {
239 emit bugReportFetched(report, job->parent());
240 } else {
241 emit bugReportError(i18nc("@info","Invalid report information (malformed data). This "
242 "could mean that the bug report does not exist, or the "
243 "bug tracking site is experiencing a problem."), job->parent());
244 }
245
246 delete parser;
247 } else {
248 emit bugReportError(job->errorString(), job->parent());
249 }
250}
251
252void BugzillaManager::searchBugsJobFinished(KJob * job)
253{
254 if (!job->error()) {
255 KIO::StoredTransferJob * searchBugsJob = static_cast<KIO::StoredTransferJob*>(job);
256
257 BugListCSVParser * parser = new BugListCSVParser(searchBugsJob->data());
258 BugMapList list = parser->parse();
259
260 if (parser->isValid()) {
261 emit searchFinished(list);
262 } else {
263 emit searchError(i18nc("@info","Invalid bug list: corrupted data"));
264 }
265
266 delete parser;
267 } else {
268 emit searchError(job->errorString());
269 }
270
271 m_searchJob = 0;
272}
273
274static inline Component buildComponent(const QVariantMap& map)
275{
276 QString name = map.value("name").toString();
277 bool active = map.value("is_active").toBool();
278
279 return Component(name, active);
280}
281
282static inline Version buildVersion(const QVariantMap& map)
283{
284 QString name = map.value("name").toString();
285 bool active = map.value("is_active").toBool();
286
287 return Version(name, active);
288}
289
290static inline Product buildProduct(const QVariantMap& map)
291{
292 QString name = map.value("name").toString();
293 bool active = map.value("is_active").toBool();
294
295 Product product(name, active);
296
297 QVariantList components = map.value("components").toList();
298 foreach (const QVariant& c, components) {
299 Component component = buildComponent(c.toMap());
300 product.addComponent(component);
301
302 }
303
304 QVariantList versions = map.value("versions").toList();
305 foreach (const QVariant& v, versions) {
306 Version version = buildVersion(v.toMap());
307 product.addVersion(version);
308 }
309
310 return product;
311}
312
313void BugzillaManager::fetchProductInfoFinished(const QVariantMap & map)
314{
315 QList<Product> products;
316
317 QVariantList plist = map.value("products").toList();
318 foreach (const QVariant& p, plist) {
319 Product product = buildProduct(p.toMap());
320 products.append(product);
321 }
322
323 if ( products.size() > 0 ) {
324 emit productInfoFetched(products.at(0));
325 } else {
326 emit productInfoError();
327 }
328}
329
330//END Slots to handle KJob::finished
331
332void BugzillaManager::callMessage(const QList<QVariant> & result, const QVariant & id)
333{
334 kDebug() << id << result;
335
336 if (id.toString() == QLatin1String("login")) {
337 m_logged = true;
338 Q_EMIT loginFinished(true);
339 } else if (id.toString() == QLatin1String("Product.get.versions")) {
340 QVariantMap map = result.at(0).toMap();
341 fetchProductInfoFinished(map);
342 } else if (id.toString() == QLatin1String("Bug.create")) {
343 QVariantMap map = result.at(0).toMap();
344 int bug_id = map.value(QLatin1String("id")).toInt();
345 Q_ASSERT(bug_id != 0);
346 Q_EMIT reportSent(bug_id);
347 } else if (id.toString() == QLatin1String("Bug.add_attachment")) {
348 QVariantMap map = result.at(0).toMap();
349 if (map.contains(QLatin1String("attachments"))){ // for bugzilla 4.2
350 map = map.value(QLatin1String("attachments")).toMap();
351 map = map.constBegin()->toMap();
352 const int attachment_id = map.value(QLatin1String("id")).toInt();
353 Q_EMIT attachToReportSent(attachment_id);
354 } else if (map.contains(QLatin1String("ids"))) { // for bugzilla 4.4
355 const int attachment_id = map.value(QLatin1String("ids")).toList().at(0).toInt();
356 Q_EMIT attachToReportSent(attachment_id);
357 }
358 } else if (id.toString() == QLatin1String("Bug.update.cc")) {
359 QVariantMap map = result.at(0).toMap().value(QLatin1String("bugs")).toList().at(0).toMap();
360 int bug_id = map.value(QLatin1String("id")).toInt();
361 Q_ASSERT(bug_id != 0);
362 Q_EMIT addMeToCCFinished(bug_id);
363 }
364}
365
366void BugzillaManager::callFault(int errorCode, const QString & errorString, const QVariant & id)
367{
368 kDebug() << id << errorCode << errorString;
369
370 QString genericError = i18nc("@info", "Received unexpected error code %1 from bugzilla. "
371 "Error message was: %2", errorCode, errorString);
372
373 if (id.toString() == QLatin1String("login")) {
374 switch(errorCode) {
375 case 300: //invalid username or password
376 Q_EMIT loginFinished(false); //TODO replace with loginError
377 break;
378 default:
379 Q_EMIT loginError(genericError);
380 break;
381 }
382 } else if (id.toString() == QLatin1String("Bug.create")) {
383 switch (errorCode) {
384 case 51: //invalid object (one example is invalid platform value)
385 case 105: //invalid component
386 case 106: //invalid product
387 Q_EMIT sendReportErrorInvalidValues();
388 break;
389 default:
390 Q_EMIT sendReportError(genericError);
391 break;
392 }
393 } else if (id.toString() == QLatin1String("Bug.add_attachment")) {
394 switch (errorCode) {
395 default:
396 Q_EMIT attachToReportError(genericError);
397 break;
398 }
399 } else if (id.toString() == QLatin1String("Bug.update.cc")) {
400 switch (errorCode) {
401 default:
402 Q_EMIT addMeToCCError(genericError);
403 break;
404 }
405 }
406}
407
408//END BugzillaManager
409
410//BEGIN BugzillaCSVParser
411
412BugListCSVParser::BugListCSVParser(const QByteArray& data)
413{
414 m_data = data;
415 m_isValid = false;
416}
417
418BugMapList BugListCSVParser::parse()
419{
420 BugMapList list;
421
422 if (!m_data.isEmpty()) {
423 //Parse buglist CSV
424 QTextStream ts(&m_data);
425 QString headersLine = ts.readLine().remove(QLatin1Char('\"')) ; //Discard headers
426 QString expectedHeadersLine = QString(columns);
427
428 if (headersLine == (QString("bug_id,") + expectedHeadersLine)) {
429 QStringList headers = expectedHeadersLine.split(',', QString::KeepEmptyParts);
430 int headersCount = headers.count();
431
432 while (!ts.atEnd()) {
433 BugMap bug; //bug report data map
434
435 QString line = ts.readLine();
436
437 //Get bug_id (always at first column)
438 int bug_id_index = line.indexOf(',');
439 QString bug_id = line.left(bug_id_index);
440 bug.insert("bug_id", bug_id);
441
442 line = line.mid(bug_id_index + 2);
443
444 QStringList fields = line.split(",\"");
445
446 for (int i = 0; i < headersCount && i < fields.count(); i++) {
447 QString field = fields.at(i);
448 field = field.left(field.size() - 1) ; //Remove trailing "
449 bug.insert(headers.at(i), field);
450 }
451
452 list.append(bug);
453 }
454
455 m_isValid = true;
456 }
457 }
458
459 return list;
460}
461
462//END BugzillaCSVParser
463
464//BEGIN BugzillaXMLParser
465
466BugReportXMLParser::BugReportXMLParser(const QByteArray & data)
467{
468 m_valid = m_xml.setContent(data, true);
469}
470
471BugReport BugReportXMLParser::parse()
472{
473 BugReport report; //creates an invalid and empty report object
474
475 if (m_valid) {
476 //Check bug notfound
477 QDomNodeList bug_number = m_xml.elementsByTagName("bug");
478 QDomNode d = bug_number.at(0);
479 QDomNamedNodeMap a = d.attributes();
480 QDomNode d2 = a.namedItem("error");
481 m_valid = d2.isNull();
482
483 if (m_valid) {
484 report.setValid(true);
485
486 //Get basic fields
487 report.setBugNumber(getSimpleValue("bug_id"));
488 report.setShortDescription(getSimpleValue("short_desc"));
489 report.setProduct(getSimpleValue("product"));
490 report.setComponent(getSimpleValue("component"));
491 report.setVersion(getSimpleValue("version"));
492 report.setOperatingSystem(getSimpleValue("op_sys"));
493 report.setBugStatus(getSimpleValue("bug_status"));
494 report.setResolution(getSimpleValue("resolution"));
495 report.setPriority(getSimpleValue("priority"));
496 report.setBugSeverity(getSimpleValue("bug_severity"));
497 report.setMarkedAsDuplicateOf(getSimpleValue("dup_id"));
498 report.setVersionFixedIn(getSimpleValue("cf_versionfixedin"));
499
500 //Parse full content + comments
501 QStringList m_commentList;
502 QDomNodeList comments = m_xml.elementsByTagName("long_desc");
503 for (int i = 0; i < comments.count(); i++) {
504 QDomElement element = comments.at(i).firstChildElement("thetext");
505 m_commentList << element.text();
506 }
507
508 report.setComments(m_commentList);
509
510 } //isValid
511 } //isValid
512
513 return report;
514}
515
516QString BugReportXMLParser::getSimpleValue(const QString & name) //Extract an unique tag from XML
517{
518 QString ret;
519
520 QDomNodeList bug_number = m_xml.elementsByTagName(name);
521 if (bug_number.count() == 1) {
522 QDomNode node = bug_number.at(0);
523 ret = node.toElement().text();
524 }
525 return ret;
526}
527
528//END BugzillaXMLParser
529
530void BugReport::setBugStatus(const QString &stat)
531{
532 setData("bug_status", stat);
533
534 m_status = parseStatus(stat);
535}
536
537void BugReport::setResolution(const QString &res)
538{
539 setData("resolution", res);
540
541 m_resolution = parseResolution(res);
542}
543
544BugReport::Status BugReport::parseStatus(const QString &stat)
545{
546 if (stat == QLatin1String("UNCONFIRMED")) {
547 return Unconfirmed;
548 } else if (stat == QLatin1String("CONFIRMED")) {
549 return New;
550 } else if (stat == QLatin1String("ASSIGNED")) {
551 return Assigned;
552 } else if (stat == QLatin1String("REOPENED")) {
553 return Reopened;
554 } else if (stat == QLatin1String("RESOLVED")) {
555 return Resolved;
556 } else if (stat == QLatin1String("NEEDSINFO")) {
557 return NeedsInfo;
558 } else if (stat == QLatin1String("VERIFIED")) {
559 return Verified;
560 } else if (stat == QLatin1String("CLOSED")) {
561 return Closed;
562 } else {
563 return UnknownStatus;
564 }
565}
566
567BugReport::Resolution BugReport::parseResolution(const QString &res)
568{
569 if (res.isEmpty()) {
570 return NotResolved;
571 } else if (res == QLatin1String("FIXED")) {
572 return Fixed;
573 } else if (res == QLatin1String("INVALID")) {
574 return Invalid;
575 } else if (res == QLatin1String("WONTFIX")) {
576 return WontFix;
577 } else if (res == QLatin1String("LATER")) {
578 return Later;
579 } else if (res == QLatin1String("REMIND")) {
580 return Remind;
581 } else if (res == QLatin1String("DUPLICATE")) {
582 return Duplicate;
583 } else if (res == QLatin1String("WORKSFORME")) {
584 return WorksForMe;
585 } else if (res == QLatin1String("MOVED")) {
586 return Moved;
587 } else if (res == QLatin1String("UPSTREAM")) {
588 return Upstream;
589 } else if (res == QLatin1String("DOWNSTREAM")) {
590 return Downstream;
591 } else if (res == QLatin1String("WAITINGFORINFO")) {
592 return WaitingForInfo;
593 } else if (res == QLatin1String("BACKTRACE")) {
594 return Backtrace;
595 } else if (res == QLatin1String("UNMAINTAINED")) {
596 return Unmaintained;
597 } else {
598 return UnknownResolution;
599 }
600}
601