diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,8 @@ include(CheckFunctionExists) include(FeatureSummary) +kde_enable_exceptions() + find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Widgets Test DBus Concurrent) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS I18n CoreAddons Service ConfigWidgets JobWidgets KIO Crash Completion XmlRpcClient WidgetsAddons Wallet Notifications IdleTime) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,4 +1,8 @@ include (CheckFunctionExists) +# FIXME: temporary +remove_definitions(-DQT_NO_CAST_FROM_ASCII) +# FIXME: temporary +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-pedantic") check_function_exists("strsignal" HAVE_STRSIGNAL) check_function_exists("uname" HAVE_UNAME) @@ -9,6 +13,7 @@ configure_file (config-drkonqi.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-drkonqi.h ) +add_subdirectory( bugzillaintegration/libbugzilla ) add_subdirectory( data ) add_subdirectory( parser ) if ( WIN32 ) @@ -62,7 +67,7 @@ bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp bugzillaintegration/reportinterface.cpp bugzillaintegration/productmapping.cpp - bugzillaintegration/parsebugbacktraces.cpp # Requires kxmlrpcclient + bugzillaintegration/parsebugbacktraces.cpp bugzillaintegration/duplicatefinderjob.cpp ) ki18n_wrap_ui(drkonqi_SRCS @@ -96,14 +101,14 @@ KF5::Completion Qt5::DBus - KF5::XmlRpcClient KF5::WidgetsAddons KF5::Wallet KF5::Notifications # for status notifier KF5::IdleTime # hide status notifier only if user saw it drkonqi_backtrace_parser + qbugzilla ) if (${Qt5X11Extras_FOUND}) target_link_libraries(drkonqi @@ -124,7 +129,6 @@ configure_file(org.kde.drkonqi.desktop.cmake ${CMAKE_BINARY_DIR}/src/org.kde.drkonqi.desktop) install(PROGRAMS ${CMAKE_BINARY_DIR}/src/org.kde.drkonqi.desktop DESTINATION ${KDE_INSTALL_APPDIR}) +#add_subdirectory( tests ) -# Only go into tests once we have a drkonqi target so the tests can reference -# it. -add_subdirectory( tests ) +add_subdirectory(bugzillaintegration/libbugzilla/autotests) diff --git a/src/bugzillaintegration/bugzillalib.h b/src/bugzillaintegration/bugzillalib.h --- a/src/bugzillaintegration/bugzillalib.h +++ b/src/bugzillaintegration/bugzillalib.h @@ -2,6 +2,7 @@ * bugzillalib.h * 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 @@ -22,335 +23,15 @@ #define BUGZILLALIB__H #include -#include -#include -#include +#include -#include +#include "bugzillaintegration/libbugzilla/bug.h" +#include "bugzillaintegration/libbugzilla/product.h" +#include "bugzillaintegration/libbugzilla/attachment.h" -namespace KIO { class Job; } +namespace KIO { class KJob; -class QString; -class QByteArray; - -//Typedefs for Bug Report Listing -typedef QMap BugMap; //Report basic fields map -typedef QList BugMapList; //List of reports - -//Main bug report data, full fields + comments -class BugReport -{ -public: - enum Status { - UnknownStatus, - Unconfirmed, - New, - Assigned, - Reopened, - Resolved, - NeedsInfo, - Verified, - Closed - }; - - enum Resolution { - UnknownResolution, - NotResolved, - Fixed, - Invalid, - WontFix, - Later, - Remind, - Duplicate, - WorksForMe, - Moved, - Upstream, - Downstream, - WaitingForInfo, - Backtrace, - Unmaintained - }; - - BugReport() - : m_isValid(false), - m_status(UnknownStatus), - m_resolution(UnknownResolution) - {} - - void setBugNumber(const QString & value) { - setData(QStringLiteral("bug_id"), value); - } - QString bugNumber() const { - return getData(QStringLiteral("bug_id")); - } - int bugNumberAsInt() const { - return getData(QStringLiteral("bug_id")).toInt(); - } - - void setShortDescription(const QString & value) { - setData(QStringLiteral("short_desc"), value); - } - QString shortDescription() const { - return getData(QStringLiteral("short_desc")); - } - - void setProduct(const QString & value) { - setData(QStringLiteral("product"), value); - } - QString product() const { - return getData(QStringLiteral("product")); - } - - void setComponent(const QString & value) { - setData(QStringLiteral("component"), value); - } - QString component() const { - return getData(QStringLiteral("component")); - } - - void setVersion(const QString & value) { - setData(QStringLiteral("version"), value); - } - QString version() const { - return getData(QStringLiteral("version")); - } - - void setOperatingSystem(const QString & value) { - setData(QStringLiteral("op_sys"), value); - } - QString operatingSystem() const { - return getData(QStringLiteral("op_sys")); - } - - void setPlatform(const QString & value) { - setData(QStringLiteral("rep_platform"), value); - } - QString platform() const { - return getData(QStringLiteral("rep_platform")); - } - - void setBugStatus(const QString &status); - QString bugStatus() const { - return getData(QStringLiteral("bug_status")); - } - - void setResolution(const QString &resolution); - QString resolution() const { - return getData(QStringLiteral("resolution")); - } - - Status statusValue() const { - return m_status; - } - - Resolution resolutionValue() const { - return m_resolution; - } - - void setPriority(const QString & value) { - setData(QStringLiteral("priority"), value); - } - QString priority() const { - return getData(QStringLiteral("priority")); - } - - void setBugSeverity(const QString & value) { - setData(QStringLiteral("bug_severity"), value); - } - QString bugSeverity() const { - return getData(QStringLiteral("bug_severity")); - } - - void setKeywords(const QStringList & keywords) { - setData(QStringLiteral("keywords"), keywords.join(QStringLiteral(","))); - } - QStringList keywords() const { - return getData(QStringLiteral("keywords")).split(QLatin1Char(',')); - } - - void setDescription(const QString & desc) { - m_commentList.insert(0, desc); - } - QString description() const { - return m_commentList.at(0); - } - - void setComments(const QStringList & comm) { - m_commentList.append(comm); - } - QStringList comments() const { - return m_commentList.mid(1); - } - - void setMarkedAsDuplicateOf(const QString & dupID) { - setData(QStringLiteral("dup_id"), dupID); - } - QString markedAsDuplicateOf() const { - return getData(QStringLiteral("dup_id")); - } - - void setVersionFixedIn(const QString & dupID) { - setData(QStringLiteral("cf_versionfixedin"), dupID); - } - QString versionFixedIn() const { - return getData(QStringLiteral("cf_versionfixedin")); - } - - void setValid(bool valid) { - m_isValid = valid; - } - bool isValid() const { - return m_isValid; - } - - /** - * @return true if the bug report is still open - * @note false does not mean, that the bug report is closed, - * as the status could be unknown - */ - bool isOpen() const { - return isOpen(m_status); - } - - static bool isOpen(Status status) { - return (status == Unconfirmed || status == New || status == Assigned || status == Reopened); - } - - /** - * @return true if the bug report is closed - * @note false does not mean, that the bug report is still open, - * as the status could be unknown - */ - bool isClosed() const { - return isClosed(m_status); - } - - static bool isClosed(Status status) { - return (status == Resolved || status == NeedsInfo || status == Verified || status == Closed); - } - - static Status parseStatus(const QString &text); - static Resolution parseResolution(const QString &text); - -private: - void setData(const QString & key, const QString & val) { - m_dataMap.insert(key, val); - } - QString getData(const QString & key) const { - return m_dataMap.value(key); - } - - bool m_isValid; - Status m_status; - Resolution m_resolution; - - BugMap m_dataMap; - QStringList m_commentList; -}; - -//XML parser that creates a BugReport object -class BugReportXMLParser -{ -public: - explicit BugReportXMLParser(const QByteArray &); - - BugReport parse(); - - bool isValid() const { - return m_valid; - } - -private: - QString getSimpleValue(const QString &); - - bool m_valid; - QDomDocument m_xml; -}; - -class BugListCSVParser -{ -public: - explicit BugListCSVParser(const QByteArray&); - - bool isValid() const { - return m_isValid; - } - - BugMapList parse(); - -private: - bool m_isValid; - QByteArray m_data; -}; - -class Component -{ -public: - Component(const QString& name, bool active): m_name(name), m_active(active) {} - - QString name() const { return m_name; } - bool active() const { return m_active; } - -private: - QString m_name; - bool m_active; -}; - -class Version -{ -public: - - Version(const QString& name, bool active): m_name(name), m_active(active) {} - - QString name() const { return m_name; } - bool active() const { return m_active; } - -private: - QString m_name; - bool m_active; -}; - - -class Product -{ -public: - - Product(const QString& name, bool active): m_name(name), m_active(active) {} - - bool isActive() const { return m_active; } - - void addComponent(const Component& component) { - m_allComponents.append(component.name()); - } - - void addVersion(const Version& version) { - m_allVersions.append(version.name()); - - if (version.active()) { - m_activeVersions.append(version.name()); - } else { - m_inactiveVersions.append(version.name()); - } - } - - QStringList components() const { return m_allComponents; } - - QStringList allVersions() const { return m_allVersions; } - QStringList activeVersions() const { return m_activeVersions; } - QStringList inactiveVersions() const { return m_inactiveVersions; } - -private: - - QString m_name; - bool m_active; - - QStringList m_allComponents; - - QStringList m_allVersions; - QStringList m_activeVersions; - QStringList m_inactiveVersions; - -}; +} class BugzillaManager : public QObject { @@ -362,83 +43,60 @@ explicit BugzillaManager(const QString &bugTrackerUrl, QObject *parent = nullptr); /* Login methods */ - void tryLogin(const QString&, const QString&); + void tryLogin(const QString &username, const QString &password); bool getLogged() const; QString getUsername() const; /* Bugzilla Action methods */ - void fetchBugReport(int, QObject * jobOwner = nullptr); - - void searchBugs(const QStringList & products, const QString & severity, - const QString & date_start, const QString & date_end , QString comment); - - void sendReport(const BugReport & report); - - void attachTextToReport(const QString & text, const QString & filename, - const QString & description, int bugId, const QString & comment); - + void fetchBugReport(int, QObject *jobOwner = nullptr); + void searchBugs(const QStringList &products, const QString &severity, + const QString &creationDate, const QString &comment); + void sendReport(const Bugzilla::NewBug &bug); + void attachTextToReport(const QString &text, const QString &filename, + const QString &description, int bugId, + const QString &comment); void addMeToCC(int bugId); - void fetchProductInfo(const QString &); /* Misc methods */ QString urlForBug(int bug_number) const; - void stopCurrentSearch(); - /* Codes for security methods used by Bugzilla in various versions. */ - enum SecurityMethod {UseCookies, UseTokens, UsePasswords}; - SecurityMethod securityMethod() { return m_security; } - private Q_SLOTS: - /* Slots to handle KJob::finished */ - void fetchBugJobFinished(KJob*); - void searchBugsJobFinished(KJob*); - void fetchProductInfoFinished(const QVariantMap&); - void lookupVersion(); - void callMessage(const QList & result, const QVariant & id); - void callFault(int errorCode, const QString & errorString, const QVariant &id); - Q_SIGNALS: /* Bugzilla actions finished successfully */ - void loginFinished(bool); - void bugReportFetched(BugReport, QObject *); - void searchFinished(const BugMapList &); - void reportSent(int); - void attachToReportSent(int); - void addMeToCCFinished(int); - void productInfoFetched(Product); + void loginFinished(bool logged); + void bugReportFetched(Bugzilla::Bug::Ptr bug, QObject *jobOwner); + void searchFinished(const QList &bug); + void reportSent(int bugId); + void attachToReportSent(int bugId); + void addMeToCCFinished(int bugId); + void productInfoFetched(const Bugzilla::Product::Ptr &product); void bugzillaVersionFound(); /* Bugzilla actions had errors */ - void loginError(const QString & errorMsg, const QString & extendedErrorMsg = QString()); - void bugReportError(const QString &, QObject *); - void searchError(const QString &); - void sendReportError(const QString & errorMsg, const QString & extendedErrorMsg = QString()); + void loginError(const QString &errorMsg, const QString & xtendedErrorMsg = QString()); + void bugReportError(const QString &errorMsg, QObject *jobOwner); + void searchError(const QString &errorMsg); + void sendReportError(const QString &errorMsg, const QString &extendedErrorMsg = QString()); void sendReportErrorInvalidValues(); //To use default values - void attachToReportError(const QString & errorMsg, const QString & extendedErrorMsg = QString()); - void addMeToCCError(const QString & errorMsg, const QString & extendedErrorMsg = QString()); + void attachToReportError(const QString &errorMsg, const QString &extendedErrorMsg = QString()); + void addMeToCCError(const QString &errorMsg, const QString &extendedErrorMsg = QString()); void productInfoError(); private: - QString m_bugTrackerUrl; - QString m_username; - QString m_token; - QString m_password; - bool m_logged; - SecurityMethod m_security; + QString m_bugTrackerUrl; + QString m_username; + QString m_token; + QString m_password; + bool m_logged = false; - KIO::Job * m_searchJob = nullptr; - KXmlRpc::Client *m_xmlRpcClient = nullptr; + KJob *m_searchJob = nullptr; - enum SecurityStatus {SecurityDisabled, SecurityEnabled}; - void callBugzilla(const char* method, const char* id, - QMap& args, - SecurityStatus security); - void setFeaturesForVersion(const QString& version); + void setFeaturesForVersion(const QString &version); }; #endif diff --git a/src/bugzillaintegration/bugzillalib.cpp b/src/bugzillaintegration/bugzillalib.cpp --- a/src/bugzillaintegration/bugzillalib.cpp +++ b/src/bugzillaintegration/bugzillalib.cpp @@ -2,6 +2,7 @@ * 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 @@ -20,60 +21,44 @@ #include "bugzillalib.h" -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include +#include "bugzillaintegration/libbugzilla/connection.h" +#include "bugzillaintegration/libbugzilla/bugzilla.h" #include "drkonqi_debug.h" -#define MAKE_BUGZILLA_VERSION(a,b,c) (((a) << 16) | ((b) << 8) | (c)) - -static const char columns[] = "bug_severity,priority,bug_status,product,short_desc,resolution"; - -//Bugzilla URLs -static const char searchUrl[] = - "buglist.cgi?query_format=advanced&order=Importance&ctype=csv" - "&product=%1" - "&longdesc_type=allwordssubstr&longdesc=%2" - "&chfieldfrom=%3&chfieldto=%4&chfield=[Bug+creation]" - "&bug_severity=%5" - "&columnlist=%6"; -// short_desc, product, long_desc(possible backtraces lines), searchFrom, searchTo, severity, columnList static const char showBugUrl[] = "show_bug.cgi?id=%1"; -static const char fetchBugUrl[] = "show_bug.cgi?id=%1&ctype=xml"; - -static inline Component buildComponent(const QVariantMap& map); -static inline Version buildVersion(const QVariantMap& map); -static inline Product buildProduct(const QVariantMap& map); -//BEGIN BugzillaManager +#warning do a pass over all lingering qasserts and qunreachables. drkonqi should not crash, so unexpected things should generally raise a signal +#warning do a pass over all qdebugs to either drop or categorize BugzillaManager::BugzillaManager(const QString &bugTrackerUrl, QObject *parent) : QObject(parent) , m_bugTrackerUrl(bugTrackerUrl) , m_logged(false) , m_searchJob(nullptr) { - m_xmlRpcClient = new KXmlRpc::Client(QUrl(m_bugTrackerUrl + QStringLiteral("xmlrpc.cgi")), this); - m_xmlRpcClient->setUserAgent(QStringLiteral("DrKonqi")); + Q_ASSERT(bugTrackerUrl.endsWith(QChar('/'))); // Allow constructors for ReportInterface and assistant dialogs to finish. - // We do not want them to be racing the remote Bugzilla database. - QMetaObject::invokeMethod (this, &BugzillaManager::lookupVersion, Qt::QueuedConnection); + // Otherwise we may have a race on our hand if the lookup finishes before + // the constructors. + QMetaObject::invokeMethod(this, &BugzillaManager::lookupVersion, Qt::QueuedConnection); } -// BEGIN Checks of Bugzilla software versions. void BugzillaManager::lookupVersion() { - QMap args; - callBugzilla("Bugzilla.version", "version", args, SecurityDisabled); + KJob *job = Bugzilla::version(); + connect(job, &KJob::finished, this, [this](KJob *job) { +#warning FIXME: version detection in the dialog is completely broken and racing with this. make the signal a queued connection for now it has no signal is the reason + try { + QString version = Bugzilla::version(job); + setFeaturesForVersion(version); + emit bugzillaVersionFound(); + } catch (Bugzilla::ProtocolException &e) { + // Version detection problems simply mean we'll not mark the version + // found and the UI will not allow reporting. + qDebug() << e.whatString(); + } + }); + job->start(); } void BugzillaManager::setFeaturesForVersion(const QString& version) @@ -101,72 +86,31 @@ 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); } - int currentVersion = MAKE_BUGZILLA_VERSION(digits.at(0).toUInt(), - digits.at(1).toUInt(), digits.at(2).toUInt()); - // Set the code(s) for historical versions of Bugzilla - before any change. - m_security = UseCookies; // Used to have cookies for update-security. - - if (currentVersion >= MAKE_BUGZILLA_VERSION(4, 4, 3)) { - // Security method changes from cookies to tokens in Bugzilla 4.4.3. - // BUT, tokens fail when kio_http sends any cookies found in KCookieJar, - // so go directly to passwords-only security (supported since Bugzilla - // 3.6 and will be enforced in Bugzilla 4.5.x). - m_security = UsePasswords; - } - - qCDebug(DRKONQI_LOG) << "VERSION" << version << "SECURITY" << m_security; + qCDebug(DRKONQI_LOG) << "VERSION" << version; } -// END Checks of Bugzilla software versions. -// BEGIN Generic remote-procedure (RPC) call to Bugzilla -void BugzillaManager::callBugzilla(const char* method, const char* id, - QMap& args, - SecurityStatus security) -{ - if (security == SecurityEnabled) { - switch (m_security) { - case UseTokens: - qCDebug(DRKONQI_LOG) << method << id << "using token"; - args.insert(QLatin1String("Bugzilla_token"), m_token); - break; - case UsePasswords: - qCDebug(DRKONQI_LOG) << method << id << "using username" << m_username; - args.insert(QLatin1String("Bugzilla_login"), m_username); - args.insert(QLatin1String("Bugzilla_password"), m_password); - break; - case UseCookies: - qCDebug(DRKONQI_LOG) << method << id << "using cookies"; - // Some KDE process other than Dr Konqi should provide cookies. - break; - } - } - - m_xmlRpcClient->call(QLatin1String(method), args, - this, SLOT(callMessage(QList,QVariant)), - this, SLOT(callFault(int,QString,QVariant)), - QLatin1String(id)); -} -// END Generic call to Bugzilla - -//BEGIN Login methods -void BugzillaManager::tryLogin(const QString& username, const QString& password) +void BugzillaManager::tryLogin(const QString &username, const QString &password) { m_username = username; - if (m_security == UsePasswords) { - m_password = password; - } + m_password = password; m_logged = false; - QMap args; - args.insert(QLatin1String("login"), username); - args.insert(QLatin1String("password"), password); - if (m_security == UseCookies) { - // Removed in Bugzilla 4.4.3 software, which no longer issues cookies. - args.insert(QLatin1String("remember"), false); - } - - callBugzilla("User.login", "login", args, SecurityDisabled); + KJob *job = Bugzilla::login(username, password); + connect(job, &KJob::finished, this, [this](KJob *job) { + try { + auto details = Bugzilla::login(job); + m_token = details.token; + Q_ASSERT(!m_token.isEmpty()); + Bugzilla::connection().setToken(m_token); + m_logged = true; + emit loginFinished(true); + } catch (Bugzilla::ProtocolException &e) { + // 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 @@ -178,114 +122,141 @@ { return m_username; } -//END Login methods -//BEGIN Bugzilla Action methods void BugzillaManager::fetchBugReport(int bugnumber, QObject * jobOwner) { - QUrl url(m_bugTrackerUrl + QString::fromLatin1(fetchBugUrl).arg(bugnumber)); + Bugzilla::Search search; + search.id = bugnumber; + +#warning this code seems a bit exessive + Bugzilla::BugClient client; + auto job = m_searchJob = Bugzilla::BugClient().search(search); + connect(job, &KJob::finished, this, [this, &client, jobOwner](KJob *job) { + if (job->error() != KJob::NoError) { +#warning fixme exceptions not handled +#warning fixme what the flip is the parnet for + emit bugReportError(job->errorString(), jobOwner); + return; + } - if (!jobOwner) { - jobOwner = this; - } + auto list = client.search(job); - KIO::Job * fetchBugJob = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo); - fetchBugJob->setParent(jobOwner); - connect(fetchBugJob, &KIO::Job::finished, this, &BugzillaManager::fetchBugJobFinished); -} + Q_ASSERT(list.size() == 1); + auto bug = list.at(0); + + Bugzilla::CommentClient commentsClient; + auto commentsJob = commentsClient.getFromBug(bug->id()); + connect(commentsJob, &KJob::finished, this, [this, &commentsClient, jobOwner, bug](KJob *job) { + auto comments = commentsClient.getFromBug(job); + qDebug() << "comments arrived" << comments.size(); + bug->setComments(comments); + m_searchJob = nullptr; +#warning fixme what the flip is the parnet for + emit bugReportFetched(bug, jobOwner); + }); + }); +} -void BugzillaManager::searchBugs(const QStringList & products, - const QString & severity, const QString & date_start, - const QString & date_end, QString comment) +void BugzillaManager::searchBugs(const QStringList &products, + const QString &severity, const QString &creationDate, + const QString &comment) { - QString product; - if (products.size() > 0) { - if (products.size() == 1) { - product = products.at(0); - } else { - Q_FOREACH(const QString & p, products) { - product += p + QStringLiteral("&product="); - } - product = product.mid(0,product.size()-9); - } - } +#warning comment not implemented. comment would filter by first backtrace function (see duplicates.cpp) but I am not sure how that is meant to work; sounds super expensive + qDebug() << products << "commenT:" << comment; + Bugzilla::Search search; + search.products = products; + search.severity = severity; + search.creationTime = creationDate; - QString url = QString(m_bugTrackerUrl) + - QString::fromLatin1(searchUrl).arg(product, comment.replace(QLatin1Char(' ') , QLatin1Char('+')), date_start, - date_end, severity, QString::fromLatin1(columns)); +#warning fixme api does not have way to force order on returned array, does that matter stopCurrentSearch(); - m_searchJob = KIO::storedGet(QUrl(url) , KIO::Reload, KIO::HideProgressInfo); - connect(m_searchJob, &KIO::Job::finished, this, &BugzillaManager::searchBugsJobFinished); + 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::ProtocolException &e) { + emit searchError(e.whatString()); + } + }); } -void BugzillaManager::sendReport(const BugReport & report) +void BugzillaManager::sendReport(const Bugzilla::NewBug &bug) { - QMap args; - args.insert(QLatin1String("product"), report.product()); - args.insert(QLatin1String("component"), report.component()); - args.insert(QLatin1String("version"), report.version()); - args.insert(QLatin1String("summary"), report.shortDescription()); - args.insert(QLatin1String("description"), report.description()); - args.insert(QLatin1String("op_sys"), report.operatingSystem()); - args.insert(QLatin1String("platform"), report.platform()); - args.insert(QLatin1String("keywords"), report.keywords()); - args.insert(QLatin1String("priority"), report.priority()); - args.insert(QLatin1String("severity"), report.bugSeverity()); - - callBugzilla("Bug.create", "Bug.create", args, SecurityEnabled); + 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::ProtocolException &e) { + emit sendReportError(e.whatString()); + } + }); } void BugzillaManager::attachTextToReport(const QString & text, const QString & filename, const QString & summary, int bugId, const QString & comment) { - QMap args; - args.insert(QLatin1String("ids"), QVariantList() << bugId); - args.insert(QLatin1String("file_name"), filename); - args.insert(QLatin1String("summary"), summary); - args.insert(QLatin1String("comment"), comment); - args.insert(QLatin1String("content_type"), QLatin1String("text/plain")); - - //data needs to be a QByteArray so that it is encoded in base64 (query.cpp:246) - args.insert(QLatin1String("data"), text.toUtf8()); - - callBugzilla("Bug.add_attachment", "Bug.add_attachment", args, - SecurityEnabled); + Bugzilla::NewAttachment attachment; + attachment.ids = QList { bugId }; + attachment.data = text; + attachment.file_name = filename; + attachment.summary = summary; + attachment.comment = comment; + attachment.content_type = QLatin1Literal("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::ProtocolException &e) { + emit attachToReportError(e.whatString()); + } + }); } void BugzillaManager::addMeToCC(int bugId) { - QMap args; - args.insert(QLatin1String("ids"), QVariantList() << bugId); - - QMap ccChanges; - ccChanges.insert(QLatin1String("add"), QVariantList() << m_username); - args.insert(QLatin1String("cc"), ccChanges); - - callBugzilla("Bug.update", "Bug.update.cc", args, SecurityEnabled); + 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, [bugId, this](KJob *) { +#warning currently not looking at response! +#warning capturing bugid in lamda + try { + Q_ASSERT(bugId != 0); + addMeToCCFinished(bugId); + } catch (Bugzilla::ProtocolException &e) { + emit addMeToCCError(e.whatString()); + } + }); } -void BugzillaManager::fetchProductInfo(const QString & product) +void BugzillaManager::fetchProductInfo(const QString &product) { - QMap args; - - args.insert(QStringLiteral("names"), (QStringList() << product) ) ; - - QStringList includeFields; - // currently we only need these informations - includeFields << QStringLiteral("name") << QStringLiteral("is_active") << QStringLiteral("components") << QStringLiteral("versions"); - - args.insert(QStringLiteral("include_fields"), includeFields) ; - - callBugzilla("Product.get", "Product.get.versions", args, SecurityDisabled); + 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::ProtocolException &e) { +#warning why does this not have a string + emit productInfoError(); + } + }); } - -//END Bugzilla Action methods - -//BEGIN Misc methods QString BugzillaManager::urlForBug(int bug_number) const { return QString(m_bugTrackerUrl) + QString::fromLatin1(showBugUrl).arg(bug_number); @@ -299,387 +270,3 @@ m_searchJob = nullptr; } } -//END Misc methods - -//BEGIN Slots to handle KJob::finished - -void BugzillaManager::fetchBugJobFinished(KJob* job) -{ - if (!job->error()) { - KIO::StoredTransferJob * fetchBugJob = static_cast(job); - - BugReportXMLParser * parser = new BugReportXMLParser(fetchBugJob->data()); - BugReport report = parser->parse(); - - if (parser->isValid()) { - emit bugReportFetched(report, job->parent()); - } else { - emit bugReportError(i18nc("@info","Invalid report information (malformed data). This " - "could mean that the bug report does not exist, or the " - "bug tracking site is experiencing a problem."), job->parent()); - } - - delete parser; - } else { - emit bugReportError(job->errorString(), job->parent()); - } -} - -void BugzillaManager::searchBugsJobFinished(KJob * job) -{ - if (!job->error()) { - KIO::StoredTransferJob * searchBugsJob = static_cast(job); - - BugListCSVParser * parser = new BugListCSVParser(searchBugsJob->data()); - BugMapList list = parser->parse(); - - if (parser->isValid()) { - emit searchFinished(list); - } else { - emit searchError(i18nc("@info","Invalid bug list: corrupted data")); - } - - delete parser; - } else { - emit searchError(job->errorString()); - } - - m_searchJob = nullptr; -} - -static inline Component buildComponent(const QVariantMap& map) -{ - QString name = map.value(QStringLiteral("name")).toString(); - bool active = map.value(QStringLiteral("is_active")).toBool(); - - return Component(name, active); -} - -static inline Version buildVersion(const QVariantMap& map) -{ - QString name = map.value(QStringLiteral("name")).toString(); - bool active = map.value(QStringLiteral("is_active")).toBool(); - - return Version(name, active); -} - -static inline Product buildProduct(const QVariantMap& map) -{ - QString name = map.value(QStringLiteral("name")).toString(); - bool active = map.value(QStringLiteral("is_active")).toBool(); - - Product product(name, active); - - QVariantList components = map.value(QStringLiteral("components")).toList(); - foreach (const QVariant& c, components) { - Component component = buildComponent(c.toMap()); - product.addComponent(component); - - } - - QVariantList versions = map.value(QStringLiteral("versions")).toList(); - foreach (const QVariant& v, versions) { - Version version = buildVersion(v.toMap()); - product.addVersion(version); - } - - return product; -} - -void BugzillaManager::fetchProductInfoFinished(const QVariantMap & map) -{ - QList products; - - QVariantList plist = map.value(QStringLiteral("products")).toList(); - foreach (const QVariant& p, plist) { - Product product = buildProduct(p.toMap()); - products.append(product); - } - - if ( products.size() > 0 ) { - emit productInfoFetched(products.at(0)); - } else { - emit productInfoError(); - } -} - -//END Slots to handle KJob::finished - -void BugzillaManager::callMessage(const QList & result, const QVariant & id) -{ - qCDebug(DRKONQI_LOG) << id << result; - - if (id.toString() == QLatin1String("login")) { - if ((m_security == UseTokens) && (result.count() > 0)) { - QVariantMap map = result.at(0).toMap(); - m_token = map.value(QLatin1String("token")).toString(); - } - m_logged = true; - Q_EMIT loginFinished(true); - } else if (id.toString() == QLatin1String("Product.get.versions")) { - QVariantMap map = result.at(0).toMap(); - fetchProductInfoFinished(map); - } else if (id.toString() == QLatin1String("Bug.create")) { - QVariantMap map = result.at(0).toMap(); - int bug_id = map.value(QLatin1String("id")).toInt(); - Q_ASSERT(bug_id != 0); - Q_EMIT reportSent(bug_id); - } else if (id.toString() == QLatin1String("Bug.add_attachment")) { - QVariantMap map = result.at(0).toMap(); - if (map.contains(QLatin1String("attachments"))){ // for bugzilla 4.2 - map = map.value(QLatin1String("attachments")).toMap(); - map = map.constBegin()->toMap(); - const int attachment_id = map.value(QLatin1String("id")).toInt(); - Q_EMIT attachToReportSent(attachment_id); - } else if (map.contains(QLatin1String("ids"))) { // for bugzilla 4.4 - const int attachment_id = map.value(QLatin1String("ids")).toList().at(0).toInt(); - Q_EMIT attachToReportSent(attachment_id); - } - } else if (id.toString() == QLatin1String("Bug.update.cc")) { - QVariantMap map = result.at(0).toMap().value(QLatin1String("bugs")).toList().at(0).toMap(); - int bug_id = map.value(QLatin1String("id")).toInt(); - Q_ASSERT(bug_id != 0); - Q_EMIT addMeToCCFinished(bug_id); - } else if (id.toString() == QLatin1String("version")) { - QVariantMap map = result.at(0).toMap(); - QString bugzillaVersion = map.value(QLatin1String("version")).toString(); - setFeaturesForVersion(bugzillaVersion); - Q_EMIT bugzillaVersionFound(); - } -} - -void BugzillaManager::callFault(int errorCode, const QString & errorString, const QVariant & id) -{ - qCDebug(DRKONQI_LOG) << id << errorCode << errorString; - - QString genericError = i18nc("@info", "Received unexpected error code %1 from bugzilla. " - "Error message was: %2", errorCode, errorString); - - if (id.toString() == QLatin1String("login")) { - switch(errorCode) { - case 300: //invalid username or password - Q_EMIT loginFinished(false); //TODO replace with loginError - break; - default: - Q_EMIT loginError(genericError); - break; - } - } else if (id.toString() == QLatin1String("Bug.create")) { - switch (errorCode) { - case 51: //invalid object (one example is invalid platform value) - case 105: //invalid component - case 106: //invalid product - Q_EMIT sendReportErrorInvalidValues(); - break; - default: - Q_EMIT sendReportError(genericError); - break; - } - } else if (id.toString() == QLatin1String("Bug.add_attachment")) { - switch (errorCode) { - default: - Q_EMIT attachToReportError(genericError); - break; - } - } else if (id.toString() == QLatin1String("Bug.update.cc")) { - switch (errorCode) { - default: - Q_EMIT addMeToCCError(genericError); - break; - } - } -} - -//END BugzillaManager - -//BEGIN BugzillaCSVParser - -BugListCSVParser::BugListCSVParser(const QByteArray& data) -{ - m_data = data; - m_isValid = false; -} - -BugMapList BugListCSVParser::parse() -{ - BugMapList list; - - if (!m_data.isEmpty()) { - //Parse buglist CSV - QTextStream ts(&m_data); - QString headersLine = ts.readLine().remove(QLatin1Char('\"')) ; //Discard headers - QString expectedHeadersLine = QString::fromLatin1(columns); - - if (headersLine == (QStringLiteral("bug_id,") + expectedHeadersLine)) { - QStringList headers = expectedHeadersLine.split(QLatin1Char(','), QString::KeepEmptyParts); - int headersCount = headers.count(); - - while (!ts.atEnd()) { - BugMap bug; //bug report data map - - QString line = ts.readLine(); - - //Get bug_id (always at first column) - int bug_id_index = line.indexOf(QLatin1Char(',')); - QString bug_id = line.left(bug_id_index); - bug.insert(QStringLiteral("bug_id"), bug_id); - - line = line.mid(bug_id_index + 2); - - QStringList fields = line.split(QStringLiteral(",\"")); - - for (int i = 0; i < headersCount && i < fields.count(); i++) { - QString field = fields.at(i); - field = field.left(field.size() - 1) ; //Remove trailing " - bug.insert(headers.at(i), field); - } - - list.append(bug); - } - - m_isValid = true; - } - } - - return list; -} - -//END BugzillaCSVParser - -//BEGIN BugzillaXMLParser - -BugReportXMLParser::BugReportXMLParser(const QByteArray & data) -{ - m_valid = m_xml.setContent(data, true); -} - -BugReport BugReportXMLParser::parse() -{ - BugReport report; //creates an invalid and empty report object - - if (m_valid) { - //Check bug notfound - QDomNodeList bug_number = m_xml.elementsByTagName(QStringLiteral("bug")); - QDomNode d = bug_number.at(0); - QDomNamedNodeMap a = d.attributes(); - QDomNode d2 = a.namedItem(QStringLiteral("error")); - m_valid = d2.isNull(); - - if (m_valid) { - report.setValid(true); - - //Get basic fields - report.setBugNumber(getSimpleValue(QStringLiteral("bug_id"))); - report.setShortDescription(getSimpleValue(QStringLiteral("short_desc"))); - report.setProduct(getSimpleValue(QStringLiteral("product"))); - report.setComponent(getSimpleValue(QStringLiteral("component"))); - report.setVersion(getSimpleValue(QStringLiteral("version"))); - report.setOperatingSystem(getSimpleValue(QStringLiteral("op_sys"))); - report.setBugStatus(getSimpleValue(QStringLiteral("bug_status"))); - report.setResolution(getSimpleValue(QStringLiteral("resolution"))); - report.setPriority(getSimpleValue(QStringLiteral("priority"))); - report.setBugSeverity(getSimpleValue(QStringLiteral("bug_severity"))); - report.setMarkedAsDuplicateOf(getSimpleValue(QStringLiteral("dup_id"))); - report.setVersionFixedIn(getSimpleValue(QStringLiteral("cf_versionfixedin"))); - - //Parse full content + comments - QStringList m_commentList; - QDomNodeList comments = m_xml.elementsByTagName(QStringLiteral("long_desc")); - for (int i = 0; i < comments.count(); i++) { - QDomElement element = comments.at(i).firstChildElement(QStringLiteral("thetext")); - m_commentList << element.text(); - } - - report.setComments(m_commentList); - - } //isValid - } //isValid - - return report; -} - -QString BugReportXMLParser::getSimpleValue(const QString & name) //Extract an unique tag from XML -{ - QString ret; - - QDomNodeList bug_number = m_xml.elementsByTagName(name); - if (bug_number.count() == 1) { - QDomNode node = bug_number.at(0); - ret = node.toElement().text(); - } - return ret; -} - -//END BugzillaXMLParser - -void BugReport::setBugStatus(const QString &stat) -{ - setData(QStringLiteral("bug_status"), stat); - - m_status = parseStatus(stat); -} - -void BugReport::setResolution(const QString &res) -{ - setData(QStringLiteral("resolution"), res); - - m_resolution = parseResolution(res); -} - -BugReport::Status BugReport::parseStatus(const QString &stat) -{ - if (stat == QLatin1String("UNCONFIRMED")) { - return Unconfirmed; - } else if (stat == QLatin1String("CONFIRMED")) { - return New; - } else if (stat == QLatin1String("ASSIGNED")) { - return Assigned; - } else if (stat == QLatin1String("REOPENED")) { - return Reopened; - } else if (stat == QLatin1String("RESOLVED")) { - return Resolved; - } else if (stat == QLatin1String("NEEDSINFO")) { - return NeedsInfo; - } else if (stat == QLatin1String("VERIFIED")) { - return Verified; - } else if (stat == QLatin1String("CLOSED")) { - return Closed; - } else { - return UnknownStatus; - } -} - -BugReport::Resolution BugReport::parseResolution(const QString &res) -{ - if (res.isEmpty()) { - return NotResolved; - } else if (res == QLatin1String("FIXED")) { - return Fixed; - } else if (res == QLatin1String("INVALID")) { - return Invalid; - } else if (res == QLatin1String("WONTFIX")) { - return WontFix; - } else if (res == QLatin1String("LATER")) { - return Later; - } else if (res == QLatin1String("REMIND")) { - return Remind; - } else if (res == QLatin1String("DUPLICATE")) { - return Duplicate; - } else if (res == QLatin1String("WORKSFORME")) { - return WorksForMe; - } else if (res == QLatin1String("MOVED")) { - return Moved; - } else if (res == QLatin1String("UPSTREAM")) { - return Upstream; - } else if (res == QLatin1String("DOWNSTREAM")) { - return Downstream; - } else if (res == QLatin1String("WAITINGFORINFO")) { - return WaitingForInfo; - } else if (res == QLatin1String("BACKTRACE")) { - return Backtrace; - } else if (res == QLatin1String("UNMAINTAINED")) { - return Unmaintained; - } else { - return UnknownResolution; - } -} diff --git a/src/bugzillaintegration/duplicatefinderjob.h b/src/bugzillaintegration/duplicatefinderjob.h --- a/src/bugzillaintegration/duplicatefinderjob.h +++ b/src/bugzillaintegration/duplicatefinderjob.h @@ -37,13 +37,6 @@ public: struct Result { - Result() - : duplicate(0), - parentDuplicate(0), - status(BugReport::UnknownStatus), - resolution(BugReport::UnknownResolution) - {} - /** * First duplicate that was found, it might be that * this one is a duplicate itself, though this is still @@ -54,18 +47,17 @@ * @note 0 means that there is no duplicate * @see parrentDuplicate */ - int duplicate; + int duplicate = 0; /** * This always points to the parent bug, i.e. * the bug that has no duplicates itself. * If this is 0 it means that there are no duplicates */ - int parentDuplicate; - - BugReport::Status status; + int parentDuplicate = 0; - BugReport::Resolution resolution; + Bugzilla::Bug::Status status = Bugzilla::Bug::Status::Unknown; + Bugzilla::Bug::Resolution resolution = Bugzilla::Bug::Resolution::Unknown; }; DuplicateFinderJob(const QList &bugIds, BugzillaManager *manager, QObject *parent = nullptr); @@ -80,12 +72,12 @@ Result result() const; private Q_SLOTS: - void slotBugReportFetched(const BugReport &bug, QObject *owner); + void slotBugReportFetched(const Bugzilla::Bug::Ptr &bug, QObject *owner); void slotBugReportError(const QString &message, QObject *owner); private: void analyzeNextBug(); - void fetchBug(const QString &bugId); + void fetchBug(int bugId); private: BugzillaManager *m_manager = nullptr; diff --git a/src/bugzillaintegration/duplicatefinderjob.cpp b/src/bugzillaintegration/duplicatefinderjob.cpp --- a/src/bugzillaintegration/duplicatefinderjob.cpp +++ b/src/bugzillaintegration/duplicatefinderjob.cpp @@ -33,8 +33,10 @@ m_bugIds(bugIds) { qCDebug(DRKONQI_LOG) << "Possible duplicates:" << m_bugIds; - connect(m_manager, &BugzillaManager::bugReportFetched, this, &DuplicateFinderJob::slotBugReportFetched); - connect(m_manager, &BugzillaManager::bugReportError, this, &DuplicateFinderJob::slotBugReportError); + connect(m_manager, &BugzillaManager::bugReportFetched, + this, &DuplicateFinderJob::slotBugReportFetched); + connect(m_manager, &BugzillaManager::bugReportError, + this, &DuplicateFinderJob::slotBugReportError); } DuplicateFinderJob::~DuplicateFinderJob() @@ -63,20 +65,18 @@ m_manager->fetchBugReport(bugId, this); } -void DuplicateFinderJob::fetchBug(const QString &bugId) +void DuplicateFinderJob::fetchBug(int bugId) { - bool ok; - const int num = bugId.toInt(&ok); - if (ok) { + if (bugId > 0) { qCDebug(DRKONQI_LOG) << "Fetching:" << bugId; - m_manager->fetchBugReport(num, this); + m_manager->fetchBugReport(bugId, this); } else { qCDebug(DRKONQI_LOG) << "Bug id not valid:" << bugId; analyzeNextBug(); } } -void DuplicateFinderJob::slotBugReportFetched(const BugReport &bug, QObject *owner) +void DuplicateFinderJob::slotBugReportFetched(const Bugzilla::Bug::Ptr &bug, QObject *owner) { if (this != owner) { return; @@ -91,31 +91,36 @@ //TODO handle more cases here if (rating != ParseBugBacktraces::PerfectDuplicate) { - qCDebug(DRKONQI_LOG) << "Bug" << bug.bugNumber() << "most likely not a duplicate:" << rating; + qCDebug(DRKONQI_LOG) << "Bug" << bug->id() << "most likely not a duplicate:" << rating; analyzeNextBug(); return; } + bool unknownStatus = (bug->status() == Bugzilla::Bug::Status::Unknown); + bool unknownResolution = (bug->resolution() == Bugzilla::Bug::Resolution::Unknown); + //The Bug is a duplicate, now find out the status and resolution of the existing report - if (bug.resolutionValue() == BugReport::Duplicate) { + if (bug->resolution() == Bugzilla::Bug::Resolution::DUPLICATE) { qCDebug(DRKONQI_LOG) << "Found duplicate is a duplicate itself."; if (!m_result.duplicate) { - m_result.duplicate = bug.bugNumberAsInt(); + m_result.duplicate = bug->id(); } - fetchBug(bug.markedAsDuplicateOf()); - } else if ((bug.statusValue() == BugReport::UnknownStatus) || (bug.resolutionValue() == BugReport::UnknownResolution)) { + fetchBug(bug->dupe_of()); +#warning fixme can unknown happen even or could it before how does this work now + } else if (unknownStatus || unknownResolution) { qCDebug(DRKONQI_LOG) << "Either the status or the resolution is unknown."; - qCDebug(DRKONQI_LOG) << "Status \"" << bug.bugStatus() << "\" known:" << (bug.statusValue() != BugReport::UnknownStatus); - qCDebug(DRKONQI_LOG) << "Resolution \"" << bug.resolution() << "\" known:" << (bug.resolutionValue() != BugReport::UnknownResolution); + qCDebug(DRKONQI_LOG) << "Status \"" << bug->status() << "\" known:" << !unknownStatus; + qCDebug(DRKONQI_LOG) << "Resolution \"" << bug->resolution() << "\" known:" << !unknownResolution; analyzeNextBug(); } else { if (!m_result.duplicate) { - m_result.duplicate = bug.bugNumberAsInt(); + m_result.duplicate = bug->id(); } - m_result.parentDuplicate = bug.bugNumberAsInt(); - m_result.status = bug.statusValue(); - m_result.resolution = bug.resolutionValue(); - qCDebug(DRKONQI_LOG) << "Found duplicate information (id/status/resolution):" << bug.bugNumber() << bug.bugStatus() << bug.resolution(); + m_result.parentDuplicate = bug->id(); + m_result.status = bug->status(); + m_result.resolution = bug->resolution(); + qCDebug(DRKONQI_LOG) << "Found duplicate information (id/status/resolution):" + << bug->id() << bug->status() << bug->resolution(); emitResult(); } } diff --git a/src/bugzillaintegration/libbugzilla/CMakeLists.txt b/src/bugzillaintegration/libbugzilla/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/CMakeLists.txt @@ -0,0 +1,24 @@ +# FIXME: temporary +remove_definitions(-DQT_NO_CAST_FROM_ASCII) + +# FIXME: temporary +set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--export-all-symbols") +# FIXME: temporary +set (CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--export-all-symbols") + +set(lib_SRCS + bug.cpp + bugzilla.cpp + connection.cpp + attachment.cpp + product.cpp + comment.cpp +) +add_library(qbugzilla STATIC ${lib_SRCS}) +target_link_libraries(qbugzilla + PUBLIC + Qt5::Core + Qt5::Network + KF5::CoreAddons + KF5::KIOCore +) diff --git a/src/bugzillaintegration/libbugzilla/attachment.h b/src/bugzillaintegration/libbugzilla/attachment.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/attachment.h @@ -0,0 +1,69 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef ATTACHMENT_H +#define ATTACHMENT_H + +#include + +#include "connection.h" + +namespace Bugzilla { + +class Bug; + +class Attachment : public QObject +{ + Q_OBJECT +public: + explicit Attachment(QObject *parent = nullptr); +}; + +struct NewAttachment +{ + QList ids; // bug ids + QString data; + QString file_name; + QString summary; + QString content_type; + QString comment; + bool is_patch; + bool is_private; + + // flags property is not supported at this time + + QByteArray toJson() const; +}; + +class AttachmentClient +{ +public: + explicit AttachmentClient(const Connection &connection = Bugzilla::connection()); + + QList createAttachment(KJob *kjob); + KJob *createAttachment(int bugId, const NewAttachment &attachment); + +private: + const Connection &m_connection; +}; + +} // namespace Bugzilla + +#endif // ATTACHMENT_H diff --git a/src/bugzillaintegration/libbugzilla/attachment.cpp b/src/bugzillaintegration/libbugzilla/attachment.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/attachment.cpp @@ -0,0 +1,87 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "attachment.h" + +#include "bug.h" + +namespace Bugzilla { + +Attachment::Attachment(QObject *parent) : QObject(parent) +{ +} + +QByteArray NewAttachment::toJson() const +{ +#warning fixme find out if there isn't a smarter way for this + QVariantList idsVariant; + for (int id : ids) { + idsVariant << QVariant::fromValue(id); + } + QVariantHash hash { + { QStringLiteral("ids"), idsVariant }, + { QStringLiteral("data"), data.toUtf8().toBase64() }, + { QStringLiteral("file_name"), file_name }, + { QStringLiteral("summary"), summary }, + { QStringLiteral("content_type"), content_type }, + { QStringLiteral("comment"), comment }, + { QStringLiteral("is_patch"), is_patch }, + { QStringLiteral("is_private"), is_private }, + }; + + QJsonDocument doc; + doc.setObject(QJsonObject::fromVariantHash(hash)); + qDebug() << doc.toJson(); + return doc.toJson(); +} + +AttachmentClient::AttachmentClient(const Connection &connection) + : m_connection(connection) +{ + +} + +QList AttachmentClient::createAttachment(KJob *kjob) +{ + auto job = qobject_cast(kjob); + Q_ASSERT(job->error() == KJob::NoError); + + auto document = job->document(); + qDebug() << document; + auto obj = document.object(); + auto ary = obj.value(QStringLiteral("ids")).toArray(); + + QList list; + for (auto x : ary) { + bool ok = false; + list.append(x.toVariant().toInt(&ok)); + Q_ASSERT(ok); + } + + return list; +} + +KJob *AttachmentClient::createAttachment(int bugId, const NewAttachment &attachment) +{ + return m_connection.post(QStringLiteral("/bug/%1/attachment").arg(bugId), + attachment.toJson()); +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/autotests/CMakeLists.txt b/src/bugzillaintegration/libbugzilla/autotests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/CMakeLists.txt @@ -0,0 +1,33 @@ +remove_definitions(-DQT_NO_CAST_FROM_ASCII) + +#set( EXPORT ) # nothing, everything is exported by default; gcc/unix + +set (CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--export-all-symbols") +set (CMAKE_MODULE_LINKER_FLAGS "${CMAKE_MODULE_LINKER_FLAGS} -Wl,--export-all-symbols") + + +include(ECMAddTests) +include(GenerateExportHeader) + +find_package(Qt5Test ${REQUIRED_QT_VERSION} CONFIG REQUIRED) + +# Include src so we have access to config-kcrash.h +include_directories(${CMAKE_CURRENT_BINARY_DIR}/..) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +ecm_add_tests( + bugtest.cpp + bugzillatest.cpp + connectiontest.cpp + producttest.cpp + LINK_LIBRARIES + Qt5::Core + Qt5::Test + Qt5::Network + + qbugzilla +) + +ecm_mark_nongui_executable(bugzillatest) + + diff --git a/src/bugzillaintegration/libbugzilla/autotests/bugtest.cpp b/src/bugzillaintegration/libbugzilla/autotests/bugtest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/bugtest.cpp @@ -0,0 +1,118 @@ +#include + +#include + +#include "../bug.h" +#include "../product.h" + +namespace Bugzilla +{ + +class JobDouble : public APIJob +{ + Q_OBJECT +public: + using APIJob::APIJob; + + JobDouble(QString fixture) + : m_fixture(fixture) + { + } + + virtual QByteArray data() const override + { + Q_ASSERT(!m_fixture.isEmpty()); + QFile file(m_fixture); + Q_ASSERT(file.open(QFile::ReadOnly | QFile::Text)); + QTextStream in(&file); + return in.readAll().toUtf8(); + } + + QString m_fixture; +}; + +class ConnectionDouble : public Connection +{ +public: + using Connection::Connection; + + virtual void setToken(const QString &) override + { + Q_UNREACHABLE(); + } + + virtual APIJob *get(const QString &path, + const QUrlQuery &query = QUrlQuery()) const override + { + if (path == "/bug" && query.toString() == "product=dragonplayer") { + return new JobDouble { QFINDTESTDATA("data/bugs.dragonplayer.json") }; + } + Q_ASSERT_X(false, "get", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *post(const QString &path, + const QByteArray &, + const QUrlQuery &query = QUrlQuery()) const override + { + qDebug() << path << query.toString(); + Q_UNREACHABLE(); + return nullptr; + } + + virtual APIJob *put(const QString &, + const QByteArray &, + const QUrlQuery & = QUrlQuery()) const override + { + Q_UNREACHABLE(); + } +}; + +class BugTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testSearch() + { + Bugzilla::Search search; + search.products = QStringList { "dragonplayer" }; + auto job = Bugzilla::BugClient().search(search); + job->start(); + QList bugs = Bugzilla::BugClient().search(job); + QCOMPARE(bugs.size(), 2); + Bug::Ptr bug; + for (auto it = bugs.begin(); it != bugs.end(); ++it) { + if ((*it)->id() == 156514) { + bug = *it; + } + } + + QCOMPARE(bug.isNull(), false); + QCOMPARE(bug->id(), 156514); + QCOMPARE(bug->product(), "dragonplayer"); + QCOMPARE(bug->component(), "general"); + QCOMPARE(bug->summary(), "Supported filetypes not shown in Play File.. Dialog"); + QCOMPARE(bug->version(), "unspecified"); + QCOMPARE(bug->op_sys(), "Linux"); + QCOMPARE(bug->priority(), "NOR"); + QCOMPARE(bug->severity(), "normal"); + QCOMPARE(bug->status(), Bug::Status::RESOLVED); + QCOMPARE(bug->resolution(), Bug::Resolution::FIXED); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::BugTest) + +#include "bugtest.moc" diff --git a/src/bugzillaintegration/libbugzilla/autotests/bugzillatest.cpp b/src/bugzillaintegration/libbugzilla/autotests/bugzillatest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/bugzillatest.cpp @@ -0,0 +1,23 @@ +#include + +#include + +#include "../bugzilla.h" + +class BugzillaTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() {} + + void testVersion() + { + qDebug() << Bugzilla::version(); + } + +#warning fixme test environment override DRKONQI_KDE_BUGZILLA_URL +}; + +QTEST_GUILESS_MAIN(BugzillaTest) + +#include "bugzillatest.moc" diff --git a/src/bugzillaintegration/libbugzilla/autotests/connectiontest.cpp b/src/bugzillaintegration/libbugzilla/autotests/connectiontest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/connectiontest.cpp @@ -0,0 +1,167 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "../connection.h" + +namespace Bugzilla +{ + +class ConnectionTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void initTestCase() + { + } + + void testGet() + { + // qhttpserver is still in qt-labs. as a simple solution do some dumb + // http socketing. + QTcpServer t; + QCOMPARE(t.listen(QHostAddress::LocalHost, 0), true); + connect(&t, &QTcpServer::newConnection, + this, [&t]() { + QTcpSocket *socket = t.nextPendingConnection(); + socket->waitForReadyRead(); + QString httpBlob = socket->readAll(); + qDebug() << httpBlob; + // The query is important to see if this actually gets properly + // passed along! + if (httpBlob.startsWith("GET /hi?informal=yes")) { + QFile file(QFINDTESTDATA("data/hi.http")); + Q_ASSERT(file.open(QFile::ReadOnly | QFile::Text)); + socket->write(file.readAll()); + socket->waitForBytesWritten(); + socket->close(); + return; + } + qDebug() << httpBlob; + Q_ASSERT_X(false, "server", "Unexpected request"); + }); + + QUrl root("http://localhost"); + root.setPort(t.serverPort()); + HTTPConnection c(root); + QUrlQuery query; + query.addQueryItem("informal", "yes"); + auto job = c.get("/hi", query); + job->exec(); + QCOMPARE(job->data(), "Hello!\n"); + } + + void testGetJsonError() + { + // qhttpserver is still in qt-labs. as a simple solution do some dumb + // http socketing. + QTcpServer t; + QCOMPARE(t.listen(QHostAddress::LocalHost, 0), true); + connect(&t, &QTcpServer::newConnection, + this, [&t]() { + QTcpSocket *socket = t.nextPendingConnection(); + socket->waitForReadyRead(); + QString httpBlob = socket->readAll(); + qDebug() << httpBlob; + QFile file(QFINDTESTDATA("data/error.http")); + QVERIFY(file.open(QFile::ReadOnly | QFile::Text)); + socket->write(file.readAll()); + socket->waitForBytesWritten(); + socket->close(); + return; + }); + + QUrl root("http://localhost"); + root.setPort(t.serverPort()); + HTTPConnection c(root); + auto job = c.get("/hi"); + job->exec(); + QCOMPARE(job->error(), 207); // error from json blob + } + + void testPut() + { + // qhttpserver is still in qt-labs. as a simple solution do some dumb + // http socketing. + QThread thread; + QTcpServer server; + server.moveToThread(&thread); + + int segments = 0; // lambda member essentially + + connect(&server, &QTcpServer::newConnection, + &server, [&server, &segments]() { + QCOMPARE(server.thread(), QThread::currentThread()); + QTcpSocket *socket = server.nextPendingConnection(); + connect(socket, &QTcpSocket::readyRead, + [&server, &segments, socket] { + ++segments; + + qDebug() << "segment" << segments; + auto data = socket->readAll(); + qDebug() << data; + if (segments == 1) { + Q_ASSERT(data.startsWith("PUT /put HTTP/")); + } else if (segments == 2) { + Q_ASSERT(data.startsWith("Content-Length: 12")); + } else if (segments == 3) { + Q_ASSERT(data == "hello there!"); + + // send reply. this is a bit awkward, but in lieu + // of a proper server we'll just assume the request + // was "complete" as our marker expectations are met. + // this is however highly dependent on KIO's behavior. + // Sooooo this test may turn flaky in the future. + + QFile file(QFINDTESTDATA("data/put.http")); + QVERIFY(file.open(QFile::ReadOnly | QFile::Text)); + QByteArray ret = file.readAll(); + ret.replace("\n", "\r\n"); + qDebug() << ret; + socket->write(ret); + socket->waitForBytesWritten(); + + socket->disconnect(); + socket->close(); + } + }); + }); + + thread.start(); + + QMutex portMutex; + QWaitCondition portCondition; + quint16 port; + portMutex.lock(); + QTimer::singleShot(0, &server, [&server, &portMutex, &portCondition, &port]() { + Q_ASSERT(server.listen(QHostAddress::LocalHost, 0)); + QMutexLocker locker(&portMutex); + port = server.serverPort(); + portCondition.wakeAll(); + }); + portCondition.wait(&portMutex); + portMutex.unlock(); + + QUrl root("http://localhost"); + root.setPort(server.serverPort()); + HTTPConnection c(root); + APIJob *job = c.put("/put", "hello there!"); + job->exec(); + + thread.quit(); + thread.wait(); + + QCOMPARE(job->error(), KJob::NoError); + } +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::ConnectionTest) + +#include "connectiontest.moc" diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugs.dragonplayer.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.dragonplayer.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.dragonplayer.json @@ -0,0 +1,115 @@ +{ + "bugs" : [ + { + "alias" : [], + "assigned_to" : "dragon-bugs@dragonplayer.org", + "assigned_to_detail" : { + "email" : "dragon-bugs@dragonplayer.org", + "id" : 94747, + "name" : "dragon-bugs@dragonplayer.org", + "real_name" : "Dragon Player Mailing List" + }, + "blocks" : [], + "cc" : [ + "kde@davidedmundson.co.uk" + ], + "cc_detail" : [ + { + "email" : "kde@davidedmundson.co.uk", + "id" : 94748, + "name" : "kde@davidedmundson.co.uk", + "real_name" : "David Edmundson" + } + ], + "cf_commitlink" : "", + "cf_versionfixedin" : "", + "classification" : "Unclassified", + "component" : "general", + "creation_time" : "2008-01-24T00:57:41Z", + "creator" : "cduquette@gmail.com", + "creator_detail" : { + "email" : "cduquette@gmail.com", + "id" : 64830, + "name" : "cduquette@gmail.com", + "real_name" : "Craig Duquette" + }, + "deadline" : null, + "depends_on" : [], + "dupe_of" : null, + "flags" : [], + "groups" : [], + "id" : 156514, + "is_cc_accessible" : true, + "is_confirmed" : false, + "is_creator_accessible" : true, + "is_open" : false, + "keywords" : [], + "last_change_time" : "2008-02-13T02:55:08Z", + "op_sys" : "Linux", + "platform" : "Compiled Sources", + "priority" : "NOR", + "product" : "dragonplayer", + "qa_contact" : "", + "resolution" : "FIXED", + "see_also" : [], + "severity" : "normal", + "status" : "RESOLVED", + "summary" : "Supported filetypes not shown in Play File.. Dialog", + "target_milestone" : "---", + "url" : "", + "version" : "unspecified", + "whiteboard" : "" + }, + { + "alias" : [], + "assigned_to" : "dragon-bugs@dragonplayer.org", + "assigned_to_detail" : { + "email" : "dragon-bugs@dragonplayer.org", + "id" : 94747, + "name" : "dragon-bugs@dragonplayer.org", + "real_name" : "Dragon Player Mailing List" + }, + "blocks" : [], + "cc" : [], + "cc_detail" : [], + "cf_commitlink" : "", + "cf_versionfixedin" : "", + "classification" : "Unclassified", + "component" : "general", + "creation_time" : "2008-01-29T15:31:36Z", + "creator" : "hansmbakker@gmail.com", + "creator_detail" : { + "email" : "hansmbakker@gmail.com", + "id" : 60871, + "name" : "hansmbakker@gmail.com", + "real_name" : "Hans Bakker" + }, + "deadline" : null, + "depends_on" : [], + "dupe_of" : null, + "flags" : [], + "groups" : [], + "id" : 156917, + "is_cc_accessible" : true, + "is_confirmed" : false, + "is_creator_accessible" : true, + "is_open" : false, + "keywords" : [], + "last_change_time" : "2008-02-13T02:55:08Z", + "op_sys" : "Linux", + "platform" : "Compiled Sources", + "priority" : "NOR", + "product" : "dragonplayer", + "qa_contact" : "", + "resolution" : "WONTFIX", + "see_also" : [], + "severity" : "wishlist", + "status" : "RESOLVED", + "summary" : "Changing settings not possible when not playing", + "target_milestone" : "---", + "url" : "", + "version" : "unspecified", + "whiteboard" : "" + } + ] +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/error.http b/src/bugzillaintegration/libbugzilla/autotests/data/error.http new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/error.http @@ -0,0 +1,5 @@ +HTTP/1.1 200 OK +Date: Tue, 05 Mar 2019 13:42:49 GMT +Server: Apache/2.4.29 (Ubuntu) + +{"error":true,"documentation":"https://bugzilla.readthedocs.org/en/5.0/api/","message":"You must enter a summary for this bug.","code":107} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/hi.http b/src/bugzillaintegration/libbugzilla/autotests/data/hi.http new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/hi.http @@ -0,0 +1,5 @@ +HTTP/1.1 200 OK +Date: Tue, 05 Mar 2019 13:42:49 GMT +Server: Apache/2.4.29 (Ubuntu) + +Hello! diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/product.dragonplayer.json b/src/bugzillaintegration/libbugzilla/autotests/data/product.dragonplayer.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/product.dragonplayer.json @@ -0,0 +1,194 @@ +{ + "products" : [ + { + "classification" : "Unclassified", + "components" : [ + { + "default_assigned_to" : "sitter@kde.org", + "default_qa_contact" : "", + "description" : "all bugs not for other components", + "flag_types" : { + "attachment" : [], + "bug" : [ + { + "cc_list" : "", + "description" : "Allows to set flags to bug fixes that need to be backported.", + "grant_group" : 6, + "id" : 23, + "is_active" : true, + "is_multiplicable" : true, + "is_requestable" : false, + "is_requesteeble" : true, + "name" : "Backport", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "All wishes submitted originating from the forum Brainstorm", + "grant_group" : 7, + "id" : 21, + "is_active" : true, + "is_multiplicable" : true, + "is_requestable" : true, + "is_requesteeble" : true, + "name" : "Brainstorm", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "Bugs that are not obvious on all setups, only reproducible in very uncommon settings", + "grant_group" : 6, + "id" : 24, + "is_active" : true, + "is_multiplicable" : true, + "is_requestable" : false, + "is_requesteeble" : true, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : true, + "is_multiplicable" : true, + "is_requestable" : true, + "is_requesteeble" : true, + "name" : "Translation_missing", + "request_group" : 7, + "sort_key" : 1 + }, + { + "cc_list" : "kde-usability@kde.org", + "description" : "This flag should be added to all reports that are usability issues", + "grant_group" : 6, + "id" : 4, + "is_active" : true, + "is_multiplicable" : true, + "is_requestable" : true, + "is_requesteeble" : true, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1200, + "is_active" : true, + "name" : "general", + "sort_key" : 0 + } + ], + "default_milestone" : "---", + "description" : "Simple Video and DVD Player [part of the kdemultimedia module]", + "has_unconfirmed" : true, + "id" : 393, + "is_active" : true, + "milestones" : [ + { + "id" : 393, + "is_active" : true, + "name" : "---", + "sort_key" : 0 + }, + { + "id" : 664, + "is_active" : true, + "name" : "2.1", + "sort_key" : 0 + }, + { + "id" : 665, + "is_active" : true, + "name" : "2.2", + "sort_key" : 0 + }, + { + "id" : 666, + "is_active" : true, + "name" : "3.0", + "sort_key" : 0 + } + ], + "name" : "dragonplayer", + "versions" : [ + { + "id" : 4408, + "is_active" : false, + "name" : "2.0", + "sort_key" : 0 + }, + { + "id" : 3240, + "is_active" : false, + "name" : "2.0-beta1", + "sort_key" : 0 + }, + { + "id" : 5106, + "is_active" : false, + "name" : "2.0-git", + "sort_key" : 0 + }, + { + "id" : 3271, + "is_active" : false, + "name" : "2.0.x", + "sort_key" : 0 + }, + { + "id" : 14902, + "is_active" : true, + "name" : "17.04", + "sort_key" : 0 + }, + { + "id" : 15110, + "is_active" : true, + "name" : "17.08", + "sort_key" : 0 + }, + { + "id" : 16546, + "is_active" : true, + "name" : "17.12", + "sort_key" : 0 + }, + { + "id" : 16778, + "is_active" : true, + "name" : "18.04", + "sort_key" : 0 + }, + { + "id" : 17542, + "is_active" : true, + "name" : "18.08", + "sort_key" : 0 + }, + { + "id" : 18356, + "is_active" : true, + "name" : "18.12", + "sort_key" : 0 + }, + { + "id" : 3272, + "is_active" : false, + "name" : "SVN", + "sort_key" : 0 + }, + { + "id" : 3239, + "is_active" : true, + "name" : "unspecified", + "sort_key" : 0 + } + ] + } + ] +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/put.http b/src/bugzillaintegration/libbugzilla/autotests/data/put.http new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/put.http @@ -0,0 +1,4 @@ +HTTP/1.1 201 Created +Date: Thu, 07 Mar 2019 12:11:29 GMT +Server: Apache/2.4.29 (Ubuntu) + diff --git a/src/bugzillaintegration/libbugzilla/autotests/producttest.cpp b/src/bugzillaintegration/libbugzilla/autotests/producttest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/producttest.cpp @@ -0,0 +1,100 @@ +#include +#include + +#include "../product.h" + +namespace Bugzilla +{ + +#warning fixme move to utils file +class JobDouble : public APIJob +{ + Q_OBJECT +public: + using APIJob::APIJob; + + JobDouble(QString fixture) + : m_fixture(fixture) + { + } + + virtual QByteArray data() const override + { + Q_ASSERT(!m_fixture.isEmpty()); + QFile file(m_fixture); + Q_ASSERT(file.open(QFile::ReadOnly | QFile::Text)); + QTextStream in(&file); + return in.readAll().toUtf8(); + } + + QString m_fixture; +}; + +#warning fixme see if we can maybe share across tests +class ConnectionDouble : public Connection +{ +public: + using Connection::Connection; + + virtual void setToken(const QString &) override + { + Q_UNREACHABLE(); + } + + virtual APIJob *get(const QString &path, + const QUrlQuery &query = QUrlQuery()) const override + { + if (path == "/product/dragonplayer") { + return new JobDouble { QFINDTESTDATA("data/product.dragonplayer.json") }; + } + Q_ASSERT_X(false, "get", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *post(const QString &path, + const QByteArray &data, + const QUrlQuery &query = QUrlQuery()) const override + { + qDebug() << path << query.toString(); + Q_UNREACHABLE(); + return nullptr; + } + + virtual APIJob *put(const QString &, + const QByteArray &, + const QUrlQuery & = QUrlQuery()) const override + { + Q_UNREACHABLE(); + } +}; + +class ProductTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testProduct() + { + KJob *job = Bugzilla::ProductClient().get("dragonplayer"); + Q_ASSERT(job); + job->start(); + Product::Ptr product = Bugzilla::ProductClient().get(job); + QCOMPARE(product->isActive(), true); + Q_ASSERT(!product->components().isEmpty()); + Q_ASSERT(!product->versions().isEmpty()); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::ProductTest) + +#include "producttest.moc" diff --git a/src/bugzillaintegration/libbugzilla/bug.h b/src/bugzillaintegration/libbugzilla/bug.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/bug.h @@ -0,0 +1,214 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef BUG_H +#define BUG_H + +#include +#include + +#include "connection.h" +#include "comment.h" + +namespace Bugzilla { + +struct Search { + QStringList products; + QString severity; + QString creationTime; + qint64 id = -1; + qint64 limit = -1; + + QUrlQuery toQuery() const; +}; + +struct NewBug { + QString product; + QString component; + QString summary; // aka shortdesc + QString version; + QString description; + QString op_sys; + QString platform; + QString priority; + QString severity; + QStringList keywords; // not documented but also supported + + QJsonDocument toJsonDocument() const; +}; + +struct BugUpdate { + struct CCUpdate { + QList add; + QList remove; + }; + + CCUpdate cc; + + QByteArray toJson() const; +}; + +class Bug : public QObject +{ +public: + enum class Status { + Unknown, // First value is default if QMetaEnum can't map the key. + UNCONFIRMED, + CONFIRMED, + ASSIGNED, + REOPENED, + RESOLVED, + NEEDSINFO, + VERIFIED, + CLOSED + }; + Q_ENUM(Status) + + enum class Resolution { + Unknown, // First value is default if QMetaEnum can't map the key. + FIXED, + INVALID, + WONTFIX, + LATER, + REMIND, + DUPLICATE, + WORKSFORME, + MOVED, + UPSTREAM, + DOWNSTREAM, + WAITINGFORINFO, + BACKTRACE, + UNMAINTAINED + }; + Q_ENUM(Resolution) + +private: + Q_OBJECT + Q_PROPERTY(qint64 id READ id WRITE setId CONSTANT) + Q_PROPERTY(QString product READ product WRITE setProduct CONSTANT) + Q_PROPERTY(QString component READ component WRITE setComponent CONSTANT) + Q_PROPERTY(QString summary READ summary WRITE setSummary CONSTANT) + Q_PROPERTY(QString version READ version WRITE setVersion CONSTANT) + Q_PROPERTY(bool is_open READ is_open WRITE setIs_open CONSTANT) + // maybe should be camel mapped, who knows + Q_PROPERTY(QString op_sys READ op_sys WRITE setOp_sys CONSTANT) + Q_PROPERTY(QString priority READ priority WRITE setPriority CONSTANT) + Q_PROPERTY(QString severity READ severity WRITE setSeverity CONSTANT) + Q_PROPERTY(Status status READ status WRITE setStatus CONSTANT) + Q_PROPERTY(Resolution resolution READ resolution WRITE setResolution CONSTANT) + Q_PROPERTY(qint64 dupe_of READ dupe_of WRITE setDupe_of CONSTANT) + + // This can be extended into with comments. It's empty by default! + Q_PROPERTY(QList comments READ comments WRITE setComments NOTIFY commentsChanged) + + // Custom fields (versionfixedin etc) are only available via customField(). + +public: + typedef QPointer Ptr; + + explicit Bug(const QVariantHash &object, QObject *parent = nullptr); + + qint64 id() const; + void setId(qint64 id); + + QVariant customField(const char *key); + + Status status() const; + void setStatus(Status status); + + Resolution resolution() const; + void setResolution(Resolution resolution); + + QString summary() const; + void setSummary(const QString &summary); + +// bool isOpen() const; +// bool isNeedingInfo() const; +// bool isClosed() const; + + QString version() const; + void setVersion(const QString &version); + + QString product() const; + void setProduct(const QString &product); + + QString component() const; + void setComponent(const QString &component); + + QString op_sys() const; + void setOp_sys(const QString &op_sys); + + QString priority() const; + void setPriority(const QString &priority); + + QString severity() const; + void setSeverity(const QString &severity); + + QList comments() const; + void setComments(const QList &comments); + + bool is_open() const; + void setIs_open(bool is_open); + + qint64 dupe_of() const; + void setDupe_of(qint64 dupe_of); + +Q_SIGNALS: + void commentsChanged(); + +private: + qint64 m_id = -1; + QString m_product; + QString m_component; + QString m_summary; + QString m_version; + bool m_is_open = false; + QString m_op_sys; + QString m_priority; + QString m_severity; + Status m_status; + Resolution m_resolution; + qint64 m_dupe_of = -1; + + QList m_comments; +}; + +class BugClient : QObject +{ + Q_OBJECT +public: + BugClient(Connection &connection = Bugzilla::connection()); + + QList search(KJob *kjob); + KJob *search(const Search &search); + + qint64 create(KJob *kjob); + KJob *create(const NewBug &bug); + + qint64 update(KJob *kjob); + KJob *update(qint64 bugId, BugUpdate &bug); + +private: + Connection &m_connection; +}; + +} // namespace Bugzilla + +#endif // BUG_H diff --git a/src/bugzillaintegration/libbugzilla/bug.cpp b/src/bugzillaintegration/libbugzilla/bug.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/bug.cpp @@ -0,0 +1,358 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "bug.h" + +#include +#include + +namespace Bugzilla { + +Bug::Bug(const QVariantHash &obj, QObject *parent) + : QObject(parent) +{ + for (auto it = obj.constBegin(); it != obj.constEnd(); ++it) { + setProperty(qPrintable(it.key()), it.value()); + } + // Enums are auto-translated from strings so long as the string is equal + // to the stringified enum key. Fail if the mapping failed. + const QString status = obj.value(QStringLiteral("status")).toString(); + Q_ASSERT_X(m_status != Status::Unknown, Q_FUNC_INFO, + qPrintable(QStringLiteral("Status mapping failed on bug %1 : %2").arg(id()).arg(status))); +#warning fixme resolution can be emptys string when unresolved, does this need special care maybe +// const QString resolution = obj.value(QStringLiteral("resolution")).toString(); +// Q_ASSERT_X(m_resolution != Resolution::Unknown, Q_FUNC_INFO, +// qPrintable(QStringLiteral("Resolution mapping failed on bug %1 : %2").arg(id()).arg(resolution))); +// qDebug() << this; +} + +Bug::Resolution Bug::resolution() const +{ + return m_resolution; +} + +void Bug::setResolution(Resolution resolution) +{ + m_resolution = resolution; +} + +QString Bug::summary() const +{ + return m_summary; +} + +void Bug::setSummary(const QString &summary) +{ + m_summary = summary; +} + +QString Bug::version() const +{ + return m_version; +} + +void Bug::setVersion(const QString &version) +{ + m_version = version; +} + +QString Bug::product() const +{ + return m_product; +} + +void Bug::setProduct(const QString &product) +{ + m_product = product; +} + +QString Bug::component() const +{ + return m_component; +} + +void Bug::setComponent(const QString &component) +{ + m_component = component; +} + +QString Bug::op_sys() const +{ + return m_op_sys; +} + +void Bug::setOp_sys(const QString &op_sys) +{ + m_op_sys = op_sys; +} + +QString Bug::priority() const +{ + return m_priority; +} + +void Bug::setPriority(const QString &priority) +{ + m_priority = priority; +} + +QString Bug::severity() const +{ + return m_severity; +} + +void Bug::setSeverity(const QString &severity) +{ + m_severity = severity; +} + +QList Bug::comments() const +{ +#warning fixme perhaps itd be good if the CommentClient could "extend", so we hide some of the logic behind retrieving the comments? + return m_comments; +} + +void Bug::setComments(const QList &comments) +{ + m_comments = comments; +} + +bool Bug::is_open() const +{ + return m_is_open; +} + +void Bug::setIs_open(bool is_open) +{ + m_is_open = is_open; +} + +qint64 Bug::dupe_of() const +{ + return m_dupe_of; +} + +void Bug::setDupe_of(qint64 dupe_of) +{ + m_dupe_of = dupe_of; +} + +qint64 Bug::id() const +{ + return m_id; +} + +void Bug::setId(qint64 id) +{ + m_id = id; +} + +QVariant Bug::customField(const char *key) +{ + return property(key); +} + +Bug::Status Bug::status() const +{ + return m_status; +} + +void Bug::setStatus(Status status) +{ + m_status = status; +} + +QUrlQuery Search::toQuery() const +{ +#warning FIXME it may be better for the serach to be a qobject and then iterate all properties and raise if a property si not mapped to a query param + QUrlQuery query; + for (const QString &product : products) { + query.addQueryItem(QStringLiteral("product"), product); + } + if (!severity.isEmpty()) { + query.addQueryItem(QStringLiteral("severity"), severity); + } + if (!creationTime.isEmpty()) { + query.addQueryItem(QStringLiteral("creation_time"), creationTime); + } + if (id > 0) { + query.addQueryItem(QStringLiteral("id"), QString::number(id)); + } + if (limit > 0) { + query.addQueryItem(QStringLiteral("limit"), QString::number(limit)); + } + + return query; +} + +QJsonDocument NewBug::toJsonDocument() const +{ + QVariantHash hash { + { QStringLiteral("product"), product }, + { QStringLiteral("component"), component }, + { QStringLiteral("summary"), summary }, + { QStringLiteral("version"), version }, + { QStringLiteral("description"), description }, + { QStringLiteral("op_sys"), op_sys }, + { QStringLiteral("platform"), platform }, + { QStringLiteral("priority"), priority }, + { QStringLiteral("severity"), severity }, + { QStringLiteral("keywords"), keywords }, + }; + + QJsonDocument doc; + doc.setObject(QJsonObject::fromVariantHash(hash)); + return doc; +} + +BugClient::BugClient(Connection &connection) + : m_connection(connection) +{ +} + +QList BugClient::search(KJob *kjob) +{ + auto job = qobject_cast(kjob); + Q_ASSERT(job->error() == KJob::NoError); + + auto document = job->document(); + auto obj = document.object(); + auto ary = obj.value(QStringLiteral("bugs")).toArray(); + + QList list; + for (auto x : ary) { + list.append(Bug::Ptr(new Bug(x.toObject().toVariantHash()))); + } + + return list; +} + +KJob *BugClient::search(const Search &search) +{ + return m_connection.get(QStringLiteral("/bug"), search.toQuery()); +} + +qint64 BugClient::create(KJob *kjob) +{ + auto job = qobject_cast(kjob); + Q_ASSERT(job->error() == KJob::NoError); + + auto document = job->document(); + qDebug() << document; + auto obj = document.object(); + qint64 ret = obj.value(QStringLiteral("id")).toInt(-1); + Q_ASSERT(ret != -1); + return ret; +} + +KJob *BugClient::create(const NewBug &bug) +{ + return m_connection.post(QStringLiteral("/bug"), + bug.toJsonDocument().toJson()); +} + +qint64 BugClient::update(KJob *kjob) +{ + auto job = qobject_cast(kjob); + Q_ASSERT(job->error() == KJob::NoError); + + auto document = job->document(); + qDebug() << document; + + APIException exception(document); + + auto obj = document.object(); + qint64 ret = obj.value(QStringLiteral("id")).toInt(-1); + Q_ASSERT(ret != -1); + return ret; +} + +KJob *BugClient::update(qint64 bugId, BugUpdate &bug) +{ + return m_connection.put(QStringLiteral("/bug/%1").arg(bugId), bug.toJson()); +} + +#warning test this it was broken +QByteArray BugUpdate::toJson() const +{ + QVariantList ccAddList; + for (auto &v : cc.add) { + Q_ASSERT(!v.isEmpty()); + ccAddList << QVariant::fromValue(v); + } + QVariantList ccRemoveList; + for (auto &v : cc.remove) { + ccRemoveList << QVariant::fromValue(v); + } + QVariantHash ccVariant { + { QStringLiteral("add"), ccAddList }, + { QStringLiteral("remove"), ccRemoveList }, + }; + + QVariantHash hash { + { QStringLiteral("cc"), ccVariant }, + }; + qDebug() << hash; + + QJsonDocument doc; + doc.setObject(QJsonObject::fromVariantHash(hash)); + return doc.toJson(); +} + +} // namespace Bugzilla + +//#warning fixme not used +//Bug::Status Bug::statusFromString(const QString &status) +//{ +// static QHash statusHash { +// { QLatin1String("UNCONFIRMED"), Status::Unconfirmed }, +// { QLatin1String("CONFIRMED"), Status::New }, +// { QLatin1String("ASSIGNED"), Status::Assigned }, +// { QLatin1String("REOPENED"), Status::Reopened }, +// { QLatin1String("RESOLVED"), Status::Resolved }, +// { QLatin1String("NEEDSINFO"), Status::NeedsInfo }, +// { QLatin1String("VERIFIED"), Status::Verified }, +// { QLatin1String("CLOSED"), Status::Closed }, +// }; + +// return statusHash.value(status, Status::Unknown); +//} + +//Bug::Resolution Bug::resolutionFromString(const QString &resolution) +//{ +// static QHash resolutionHash { +// { QStringLiteral(""), Resolution::NotResolved }, +// { QStringLiteral("FIXED"), Resolution::Fixed }, +// { QStringLiteral("INVALID"), Resolution::Invalid }, +// { QStringLiteral("WONTFIX"), Resolution::WontFix }, +// { QStringLiteral("LATER"), Resolution::Later }, +// { QStringLiteral("REMIND"), Resolution::Remind }, +// { QStringLiteral("DUPLICATE"), Resolution::Duplicate }, +// { QStringLiteral("WORKSFORME"), Resolution::WorksForMe }, +// { QStringLiteral("MOVED"), Resolution::Moved }, +// { QStringLiteral("UPSTREAM"), Resolution::Upstream }, +// { QStringLiteral("DOWNSTREAM"), Resolution::Downstream }, +// { QStringLiteral("WAITINGFORINFO"), Resolution::WaitingForInfo }, +// { QStringLiteral("BACKTRACE"), Resolution::Backtrace }, +// { QStringLiteral("UNMAINTAINED"), Resolution::Unmaintained }, +// }; + +// return resolutionHash.value(resolution, Resolution::Unknown); +//} diff --git a/src/bugzillaintegration/libbugzilla/bugzilla.h b/src/bugzillaintegration/libbugzilla/bugzilla.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/bugzilla.h @@ -0,0 +1,43 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef BUGZILLA_H +#define BUGZILLA_H + +#include "connection.h" + +namespace Bugzilla +{ + struct LoginDetails { + int id; + QString token; + }; + + QString version(KJob *kjob); + APIJob *version(const Connection &connection = Bugzilla::connection()); + + // https://bugzilla.readthedocs.io/en/5.0/api/core/v1/user.html#login + LoginDetails login(KJob *kjob); + APIJob *login(const QString &username, + const QString &password, + const Connection &connection = Bugzilla::connection()); +} // namespace Bugzilla + +#endif // BUGZILLA_H diff --git a/src/bugzillaintegration/libbugzilla/bugzilla.cpp b/src/bugzillaintegration/libbugzilla/bugzilla.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/bugzilla.cpp @@ -0,0 +1,58 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "bugzilla.h" + +namespace Bugzilla { + +QString version(KJob *kjob) +{ + auto job = qobject_cast(kjob); + auto document = job->document(); + QString version = document.object().value(QLatin1String("version")).toString(); + return version; +} + +APIJob *version(const Connection &connection) +{ + return connection.get(QStringLiteral("/version")); +} + +LoginDetails login(KJob *kjob) +{ + auto job = qobject_cast(kjob); + auto document = job->document(); + QString token = document.object().value(QLatin1String("token")).toString(); + int id = document.object().value(QLatin1String("id")).toInt(-1); +#warning FIXME return something and set on connection. maybe we should return a connection with details set? + return LoginDetails { id, token }; +} + +APIJob *login(const QString &username, const QString &password, const Connection &connection) +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("login"), username); + query.addQueryItem(QStringLiteral("password"), password); + query.addQueryItem(QStringLiteral("restrict_login"), QStringLiteral("true")); + return connection.get(QStringLiteral("/login"), query); +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/comment.h b/src/bugzillaintegration/libbugzilla/comment.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/comment.h @@ -0,0 +1,64 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef COMMENT_H +#define COMMENT_H + +#include "connection.h" + +namespace Bugzilla { + +class Comment : public QObject +{ + Q_OBJECT + Q_PROPERTY(int bug_id READ bug_id WRITE setBug_id CONSTANT) + Q_PROPERTY(QString text READ text WRITE setText CONSTANT) +public: + typedef QPointer Ptr; + + explicit Comment(const QVariantHash &object, QObject *parent = nullptr); + + int bug_id() const; + void setBug_id(int bug_id); + + QString text() const; + void setText(const QString &text); + +private: + int m_bug_id; + QString m_text; +}; + +class CommentClient +{ +public: + explicit CommentClient(const Connection &connection = Bugzilla::connection()); + + QList getFromBug(KJob *kjob); + KJob *getFromBug(int bugId); + +private: + const Connection &m_connection; +}; + + +} // namespace Bugzilla + +#endif // COMMENT_H diff --git a/src/bugzillaintegration/libbugzilla/comment.cpp b/src/bugzillaintegration/libbugzilla/comment.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/comment.cpp @@ -0,0 +1,87 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "comment.h" + +namespace Bugzilla { + +Comment::Comment(const QVariantHash &object, QObject *parent) + : QObject(parent) +{ + for (auto it = object.constBegin(); it != object.constEnd(); ++it) { + setProperty(qPrintable(it.key()), it.value()); + } +} + +int Comment::bug_id() const +{ + return m_bug_id; +} + +void Comment::setBug_id(int bug_id) +{ + m_bug_id = bug_id; +} + +QString Comment::text() const +{ + return m_text; +} + +void Comment::setText(const QString &text) +{ + m_text = text; +} + + +CommentClient::CommentClient(const Connection &connection) + : m_connection(connection) +{ +} + +#warning fimxe somehow bug should do this, feels super weird to have to ask antoher client to ask for comments. could just be the bug model. but then the bug model knows more? +QList CommentClient::getFromBug(KJob *kjob) +{ + auto job = qobject_cast(kjob); + + auto document = job->document(); + QJsonObject bugs = document.object().value(QStringLiteral("bugs")).toObject(); + qDebug() << bugs; + Q_ASSERT(bugs.keys().size() == 1); + qDebug() << bugs.keys().at(0); + QJsonObject bug = bugs.value(bugs.keys().at(0)).toObject(); + qDebug() << bug; + QJsonArray comments = bug.value(QStringLiteral("comments")).toArray(); + qDebug() << comments; + + QList list; + for (auto it = comments.constBegin(); it != comments.constEnd(); ++it) { + list.append(new Comment((*it).toObject().toVariantHash())); + } + + return list; +} + +KJob *CommentClient::getFromBug(int bugId) +{ + return m_connection.get(QStringLiteral("/bug/%1/comment").arg(QString::number(bugId))); +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/connection.h b/src/bugzillaintegration/libbugzilla/connection.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/connection.h @@ -0,0 +1,175 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef CONNECTION_H +#define CONNECTION_H + +#include +#include + +#include + +#include + +#include +#include + +#include + +#include +#include +#include + +class QNetworkAccessManager; + +typedef QPointer QNReqPtr; +typedef QPointer QNRepPtr; + +namespace KIO { +class TransferJob; +} + +namespace Bugzilla { + +class APIJob; + +class Exception : public QException +{ +public: + using QException::QException; + + virtual QString whatString() const = 0; + virtual const char *what() const noexcept override; +}; + +class ProtocolException : public Exception +{ +public: + ProtocolException(const APIJob *job); + ProtocolException(const ProtocolException &other); + + virtual void raise() const override { throw *this; } + virtual ProtocolException *clone() const override { return new ProtocolException(*this); } + virtual QString whatString() const override; + + static void maybeThrow(const APIJob *job); + +private: + const APIJob *m_job = nullptr; +}; + +class APIException : public Exception +{ +public: + APIException(const QJsonDocument &document); + APIException(const APIException &other); + + virtual void raise() const override { throw *this; } + virtual APIException *clone() const override { return new APIException(*this); } + virtual QString whatString() const override; + + bool isError() const { return m_isError; } + + static void maybeThrow(const QJsonDocument &document); + +private: + bool m_isError = false; + QString m_message; + int m_code = -1; +}; + +class APIJob : public KJob +{ + Q_OBJECT +public: + enum { + UnknownError = KJob::UserDefinedError, + }; + + using KJob::KJob; + + QJsonDocument document() const; + + // Should be protected but since we call it for testing I don't care. + virtual QByteArray data() const = 0; + +private: + virtual void start() override {} + virtual void connectNotify(const QMetaMethod &signal) override; +}; + +class TransferAPIJob : public APIJob +{ + Q_OBJECT + friend class HTTPConnection; +public: + virtual QByteArray data() const override { return m_data; } + +private: + explicit TransferAPIJob(KIO::TransferJob *transferJob, QObject *parent = nullptr); + + void setPutData(const QByteArray &data); + void addMetaData(const QString &key, const QString &value); + + KIO::TransferJob *m_transferJob = nullptr; + QByteArray m_data; + QByteArray m_putData; + QList m_dataSegments; +}; + +class Connection : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; + + virtual void setToken(const QString &authToken) = 0; + + virtual APIJob *get(const QString &path, const QUrlQuery &query = QUrlQuery()) const = 0; + virtual APIJob *post(const QString &path, const QByteArray &data, const QUrlQuery &query = QUrlQuery()) const = 0; + virtual APIJob *put(const QString &path, const QByteArray &data, const QUrlQuery &query = QUrlQuery()) const = 0; +}; + +class HTTPConnection : public Connection +{ + Q_OBJECT +public: + explicit HTTPConnection(const QUrl &root = QUrl(), QObject *parent = nullptr); + ~HTTPConnection(); + + virtual void setToken(const QString &authToken) override; + + virtual APIJob *get(const QString &path, const QUrlQuery &query = QUrlQuery()) const override; + virtual APIJob *post(const QString &path, const QByteArray &data, const QUrlQuery &query = QUrlQuery()) const override; + virtual APIJob *put(const QString &path, const QByteArray &data, const QUrlQuery &query = QUrlQuery()) const override; + +private: + QUrl url(const QString &appendix, QUrlQuery query) const; + + QUrl m_root; + QString m_token; +}; + +Connection &connection(); +void setConnection(Connection *newConnection); + +} // namespace Bugzilla + +#endif // CONNECTION_H diff --git a/src/bugzillaintegration/libbugzilla/connection.cpp b/src/bugzillaintegration/libbugzilla/connection.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/connection.cpp @@ -0,0 +1,281 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "connection.h" + +#include +#include +#include + +#include + +namespace Bugzilla { + +APIException::APIException(const QJsonDocument &document) +{ + QJsonObject object = document.object(); + if (object.isEmpty()) { + return; + } + m_isError = object.value("error").toBool(m_isError); + m_message = object.value("message").toString(m_message); + m_code = object.value("code").toInt(m_code); +} + +APIException::APIException(const APIException &other) + : m_isError(other.m_isError) + , m_message(other.m_message) + , m_code(other.m_code) +{ +} + +QString APIException::whatString() const +{ + return QStringLiteral("[%1] %2").arg(m_code).arg(m_message); +} + +void APIException::maybeThrow(const QJsonDocument &document) +{ + APIException ex(document); + + if (ex.isError()) { + ex.raise(); + } +} + +HTTPConnection::HTTPConnection(const QUrl &root, QObject *parent) + : Connection(parent) + , m_root(QStringLiteral("http://bugstest.kde.org/rest")) +{ + if (!root.isEmpty()) { + m_root = QUrl(root); + } + const QString override = qEnvironmentVariable("DRKONQI_KDE_BUGZILLA_URL"); + if (!override.isEmpty()) { + m_root = QUrl(override + QStringLiteral("/rest")); + } +} + +HTTPConnection::~HTTPConnection() +{ + qDebug() << "-----------------------------------"; +} + +void HTTPConnection::setToken(const QString &authToken) +{ + m_token = authToken; +} + +APIJob *HTTPConnection::get(const QString &path, const QUrlQuery &query) const +{ + auto job = new TransferAPIJob(KIO::get(url(path, query), KIO::Reload, KIO::HideProgressInfo)); + return job; +} + +APIJob *HTTPConnection::post(const QString &path, const QByteArray &data, const QUrlQuery &query) const +{ + auto job = new TransferAPIJob(KIO::http_post(url(path, query), data, KIO::HideProgressInfo)); + return job; +} + +APIJob *HTTPConnection::put(const QString &path, const QByteArray &data, const QUrlQuery &query) const +{ + auto job = new TransferAPIJob(KIO::put(url(path, query), KIO::HideProgressInfo)); + job->setPutData(data); + return job; +} + +QUrl HTTPConnection::url(const QString &appendix, QUrlQuery query) const +{ + QUrl url(m_root); + url.setPath(m_root.path() + appendix); + + qDebug() << url; + if (!m_token.isEmpty()) { + query.addQueryItem(QStringLiteral("token"), m_token); + } + + url.setQuery(query); + qDebug() << url; + return url; +} + +TransferAPIJob::TransferAPIJob(KIO::TransferJob *transferJob, QObject *parent) + : APIJob(parent) + , m_transferJob(transferJob) +{ + // Required for every request type. + addMetaData(QStringLiteral("content-type"), QStringLiteral("application/json")); + addMetaData(QStringLiteral("accept"), QStringLiteral("application/json")); + addMetaData(QStringLiteral("UserAgent"), QStringLiteral("DrKonqi")); + + connect(m_transferJob, &KIO::TransferJob::data, + this, [this](KIO::Job *, const QByteArray &data) { + m_data += data; + }); + + connect(m_transferJob, &KIO::TransferJob::finished, + this, [this](KJob *job) { + setError(job->error()); + setErrorText(job->errorText()); + +#warning this is kinda replaced by APIException + if (error() == KJob::NoError) { + // If we have an API error the HTTP code will be fine even though + // there was an error on the API level. + // This only applies if there was not a higher level http error! + auto object = QJsonDocument::fromJson(data()).object(); + bool error = object.value("error").toBool(false); + if (error) { +#warning depth is a bit intense here +#warning should we maybe make an error object that manages deserialization? + const QString message = object.value("message").toString(); + const int code = object.value("code").toInt(); + const QString errorText = + QStringLiteral("[%1] %2").arg(code).arg(message); + setErrorText(errorText); + // NB: the code technicallly can conflict with KIO's errors, since + // we don't attach meaning to the codes beyond KJob's that + // should not make a difference though. + setError(KJob::UserDefinedError + object.value("code").toInt()); + } + } +#warning the error managing is a bit garbage... errorText is not localized, but at the same time bugzilla has no well defined codes we could translate + + emitResult(); + }); +} + +void TransferAPIJob::addMetaData(const QString &key, const QString &value) +{ + m_transferJob->addMetaData(key, value); +} + +void TransferAPIJob::setPutData(const QByteArray &data) +{ + m_putData = data; + + // This is rally awkward, does it need to be this way? Why can't we just + // push the entire array in? + + // dataReq says we shouldn't send data >1mb, so segment the incoming data + // accordingly and generate QBAs wrapping the raw data (zero-copy). + int segmentSize = 1024 * 1024; // 1 mb per segment maximum + int segments = qMax(data.size() / segmentSize, 1); + m_dataSegments.reserve(segments); + for (int i = 0; i < segments; ++i) { + int offset = i * segmentSize; + const char *buf = data.constData() + offset; + int segmentLength = qMin(offset + segmentSize, data.size()); + m_dataSegments.append(QByteArray::fromRawData(buf, segmentLength)); + } + + // TODO: throw away, only here to make sure I don't mess up the + // segmentation. + int allLengths = 0; + for (auto a : m_dataSegments) { + allLengths += a.size(); + } + Q_ASSERT(allLengths == data.size()); + + connect(m_transferJob, &KIO::TransferJob::dataReq, + this, [this](KIO::Job *, QByteArray &dataForSending) { + if (m_dataSegments.isEmpty()) { + return; + } + dataForSending = m_dataSegments.takeFirst(); + }); +} + +QJsonDocument APIJob::document() const +{ + ProtocolException::maybeThrow(this); + Q_ASSERT(error() == KJob::NoError); + + auto document = QJsonDocument::fromJson(data()); + APIException::maybeThrow(document); + return document; +} + +void APIJob::connectNotify(const QMetaMethod &signal) +{ + if (signal == QMetaMethod::fromSignal(&KJob::finished)) { + qDebug() << "auto starting"; + start(); + } + KJob::connectNotify(signal); +} + +class GlobalConnection +{ +public: + ~GlobalConnection() + { + delete m_connection; + } + + Connection *m_connection = new HTTPConnection; +}; + +Q_GLOBAL_STATIC(GlobalConnection, s_connection) + +Connection &connection() +{ + return *(s_connection->m_connection); +} + +void setConnection(Connection *newConnection) +{ + delete s_connection->m_connection; + s_connection->m_connection = newConnection; +} + +ProtocolException::ProtocolException(const APIJob *job) + : Exception() + , m_job(job) +{ +} + +ProtocolException::ProtocolException(const ProtocolException &other) + : m_job(other.m_job) +{ +} + +QString ProtocolException::whatString() const +{ + // String generally includes the error code, so no extra logic needed. + return m_job->errorString(); +} + +void ProtocolException::maybeThrow(const APIJob *job) +{ + if (job->error() == KJob::NoError) { + return; + } + throw ProtocolException(job); +} + +const char *Exception::what() const noexcept +{ + return qUtf8Printable(whatString()); +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/product.h b/src/bugzillaintegration/libbugzilla/product.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/product.h @@ -0,0 +1,125 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#ifndef PRODUCT_H +#define PRODUCT_H + +#include + +#include "connection.h" + +namespace Bugzilla { + +class ProductVersion : public QObject +{ + Q_OBJECT + Q_PROPERTY(int id READ id WRITE setId CONSTANT) + Q_PROPERTY(QString name READ name WRITE setName CONSTANT) + Q_PROPERTY(bool active READ isActive WRITE setActive CONSTANT) +public: + int id() const { return m_id; } + QString name() const { return m_name; } + bool isActive() const { return m_active; } + + explicit ProductVersion(const QVariantHash &object, QObject *parent = nullptr); +private: + void setId(int id) { m_id = id; } + void setName(const QString &name) { m_name = name; } + void setActive(bool active) { m_active = active; } + + int m_id = -1; + QString m_name = QString(); + bool m_active = false; +}; + +class ProductComponent : public QObject +{ + Q_OBJECT + Q_PROPERTY(int id READ id WRITE setId CONSTANT) + Q_PROPERTY(QString name READ name WRITE setName CONSTANT) +public: + int id() const { return m_id; } + QString name() const { return m_name; } + + explicit ProductComponent(const QVariantHash &object, QObject *parent = nullptr); +private: + void setId(int id) { m_id = id; } + void setName(const QString &name) { m_name = name; } + + int m_id = -1; + QString m_name = QString(); +}; + +class Product : public QObject +{ + Q_OBJECT + Q_PROPERTY(bool is_active READ isActive WRITE setActive CONSTANT) + Q_PROPERTY(QList components READ getComponents WRITE setComponents CONSTANT) + Q_PROPERTY(QList versions READ versions WRITE setVersions CONSTANT) +public: + typedef QSharedPointer Ptr; + + explicit Product(const QVariantHash &object, + const Connection &connection = Bugzilla::connection(), + QObject *parent = nullptr); + ~Product(); + + QStringList components() const; + QStringList allVersions() const; + QStringList activeVersions() const; + QStringList inactiveVersions() const; + + bool isActive() const; + void setActive(bool active); + + QList getComponents() const; + void setComponents(const QList &components); + + QList versions() const; + void setVersions(const QList &versions); + +private: + const Connection &m_connection; + + bool m_active = false; + QList m_components; + QList m_versions; +}; + +class ProductClient +{ +public: + explicit ProductClient(const Connection &connection = Bugzilla::connection()); + + // Gets a single Product by its name + Product::Ptr get(KJob *kjob); + KJob *get(const QString &idOrName); + +private: + const Connection &m_connection; +}; + + +} // namespace Bugzilla + +Q_DECLARE_METATYPE(Bugzilla::ProductComponent *) +Q_DECLARE_METATYPE(QList) + +#endif // PRODUCT_H diff --git a/src/bugzillaintegration/libbugzilla/product.cpp b/src/bugzillaintegration/libbugzilla/product.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/product.cpp @@ -0,0 +1,166 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "product.h" + +#include +#include +#include + +namespace Bugzilla { + +Product::Product(const QVariantHash &object, const Connection &connection, QObject *parent) + : QObject(parent) + , m_connection(connection) +{ +#warning there may be a better way but I really cannot find it + QMetaType::registerConverter>([](QVariantList v) -> QList + { + QList list; + list.reserve(v.size()); + for (const QVariant &variant : v) { + list.append(new ProductComponent(variant.toHash())); + } + return list; + }); + + QMetaType::registerConverter>([](QVariantList v) -> QList + { + QList list; + list.reserve(v.size()); + for (const QVariant &variant : v) { + list.append(new ProductVersion(variant.toHash())); + } + return list; + }); + + for (auto it = object.constBegin(); it != object.constEnd(); ++it) { + setProperty(qPrintable(it.key()), it.value()); + } +} + +QList Product::versions() const +{ + return m_versions; +} + +void Product::setVersions(const QList &versions) +{ + m_versions = versions; +} + +#warning fixme get prefix to allow for compat helper components +QList Product::getComponents() const +{ + return m_components; +} + +void Product::setComponents(const QList &components) +{ + m_components = components; +} + +Product::~Product() +{ + qDeleteAll(m_components); + qDeleteAll(m_versions); +} + +bool Product::isActive() const +{ + return m_active; +} + +#warning COMPAT COMPAT CIMPAT +QStringList Product::components() const +{ + QStringList ret; + for (const auto *component : m_components) { + ret << component->name(); + } + return ret; +} +#warning COMPAT COMPAT CIMPAT +QStringList Product::allVersions() const +{ + QStringList ret; + for (const auto *version : m_versions) { + ret << version->name(); + } + return ret; +} +#warning COMPAT COMPAT CIMPAT +QStringList Product::inactiveVersions() const +{ + QStringList ret; + for (const auto *version : m_versions) { + if (!version->isActive()) { + ret << version->name(); + } + } + return ret; +} + +void Product::setActive(bool active) +{ + m_active = active; +} + +ProductVersion::ProductVersion(const QVariantHash &object, QObject *parent) + : QObject(parent) +{ + for (auto it = object.constBegin(); it != object.constEnd(); ++it) { + setProperty(qPrintable(it.key()), it.value()); + } +} + +ProductComponent::ProductComponent(const QVariantHash &object, QObject *parent) + : QObject(parent) +{ + for (auto it = object.constBegin(); it != object.constEnd(); ++it) { + setProperty(qPrintable(it.key()), it.value()); + } +} + +ProductClient::ProductClient(const Connection &connection) + : m_connection(connection) +{ +} + +Product::Ptr ProductClient::get(KJob *kjob) +{ + auto job = qobject_cast(kjob); + Q_ASSERT(job->error() == KJob::NoError); + + auto document = job->document(); + const QJsonArray productsArray = document.object().value(QLatin1String("products")).toArray(); + Q_ASSERT(productsArray.size() == 1); + + auto obj = productsArray.at(0).toObject().toVariantHash(); + + return Product::Ptr(new Product(obj, m_connection)); +} + +KJob *ProductClient::get(const QString &idOrName) +{ + return m_connection.get(QStringLiteral("/product/%1").arg(idOrName)); +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/parsebugbacktraces.h b/src/bugzillaintegration/parsebugbacktraces.h --- a/src/bugzillaintegration/parsebugbacktraces.h +++ b/src/bugzillaintegration/parsebugbacktraces.h @@ -34,7 +34,7 @@ { Q_OBJECT public: - explicit ParseBugBacktraces(const BugReport &bug, QObject *parent = nullptr); + explicit ParseBugBacktraces(const Bugzilla::Bug::Ptr &bug, QObject *parent = nullptr); void parse(); @@ -56,7 +56,7 @@ private: BacktraceParser *m_parser = nullptr; - const BugReport m_bug; + const Bugzilla::Bug::Ptr m_bug; QList > m_backtraces; }; diff --git a/src/bugzillaintegration/parsebugbacktraces.cpp b/src/bugzillaintegration/parsebugbacktraces.cpp --- a/src/bugzillaintegration/parsebugbacktraces.cpp +++ b/src/bugzillaintegration/parsebugbacktraces.cpp @@ -110,7 +110,7 @@ } } -ParseBugBacktraces::ParseBugBacktraces(const BugReport &bug, QObject *parent) +ParseBugBacktraces::ParseBugBacktraces(const Bugzilla::Bug::Ptr &bug, QObject *parent) : QObject(parent), m_bug(bug) { @@ -120,11 +120,9 @@ void ParseBugBacktraces::parse() { - parse(m_bug.description()); - - QStringList comments = m_bug.comments(); - foreach (const QString &comment, comments) { - parse(comment); + QList comments = m_bug->comments(); + foreach (const auto &comment, comments) { + parse(comment->text()); } } diff --git a/src/bugzillaintegration/productmapping.h b/src/bugzillaintegration/productmapping.h --- a/src/bugzillaintegration/productmapping.h +++ b/src/bugzillaintegration/productmapping.h @@ -24,15 +24,17 @@ #include #include +#include "bugzillaintegration/libbugzilla/product.h" + class Product; class BugzillaManager; class CrashedApplication; class ProductMapping: public QObject { Q_OBJECT public: - explicit ProductMapping(const CrashedApplication *, BugzillaManager *, QObject * parent = nullptr); + explicit ProductMapping(const CrashedApplication *, BugzillaManager *, QObject *parent = nullptr); QString bugzillaProduct() const; QString bugzillaComponent() const; @@ -43,25 +45,24 @@ bool bugzillaVersionDisabled() const; private Q_SLOTS: - void checkProductInfo(const Product &); + void checkProductInfo(const Bugzilla::Product::Ptr); private: void map(const QString&); void mapUsingInternalFile(const QString&); void getRelatedProductsUsingInternalFile(const QString&); QStringList m_relatedBugzillaProducts; - QString m_bugzillaProduct; - QString m_bugzillaComponent; + QString m_bugzillaProduct; + QString m_bugzillaComponent; - QString m_bugzillaVersionString; + QString m_bugzillaVersionString; - const CrashedApplication * m_crashedAppPtr; - BugzillaManager * m_bugzillaManagerPtr = nullptr; + const CrashedApplication *m_crashedAppPtr = nullptr; + BugzillaManager *m_bugzillaManagerPtr = nullptr; bool m_bugzillaProductDisabled; bool m_bugzillaVersionDisabled; - }; #endif diff --git a/src/bugzillaintegration/productmapping.cpp b/src/bugzillaintegration/productmapping.cpp --- a/src/bugzillaintegration/productmapping.cpp +++ b/src/bugzillaintegration/productmapping.cpp @@ -121,20 +121,22 @@ } } -void ProductMapping::checkProductInfo(const Product & product) +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(); + m_bugzillaProductDisabled = !product->isActive(); // check whether the product on bugzilla contains the expected component - if (! product.components().contains(m_bugzillaComponent)) { + if (! product->components().contains(m_bugzillaComponent)) { m_bugzillaComponent = QLatin1String("general"); } + qDebug() << "components: " << product->components(); + qDebug() << "versions: " << product->allVersions(); // find the appropriate version to use on bugzilla const QString version = m_crashedAppPtr->version(); - const QStringList& allVersions = product.allVersions(); + const QStringList &allVersions = product->allVersions(); if (allVersions.contains(version)) { //The version the crash application provided is a valid bugzilla version: use it ! @@ -149,7 +151,7 @@ // check whether that verions is disabled for new reports, which // usually means that version is outdated and not supported anymore. - const QStringList& inactiveVersions = product.inactiveVersions(); + const QStringList& inactiveVersions = product->inactiveVersions(); m_bugzillaVersionDisabled = inactiveVersions.contains(m_bugzillaVersionString); } diff --git a/src/bugzillaintegration/reportassistantpages_base.cpp b/src/bugzillaintegration/reportassistantpages_base.cpp --- a/src/bugzillaintegration/reportassistantpages_base.cpp +++ b/src/bugzillaintegration/reportassistantpages_base.cpp @@ -130,6 +130,13 @@ //BEGIN BugAwarenessPage +static QHash s_reproducibleIndex { + { 0, ReportInterface::ReproducibleUnsure }, + { 1, ReportInterface::ReproducibleNever }, + { 2, ReportInterface::ReproducibleSometimes }, + { 3, ReportInterface::ReproducibleEverytime } +}; + BugAwarenessPage::BugAwarenessPage(ReportAssistantDialog * parent) : ReportAssistantPage(parent) { @@ -148,6 +155,12 @@ ui.m_appSpecificDetailsExamples->setContextMenuPolicy(Qt::NoContextMenu); connect(ui.m_appSpecificDetailsExamples, &QLabel::linkActivated, this, &BugAwarenessPage::showApplicationDetailsExamples); + + if (qEnvironmentVariableIsSet("DRKONQI_TEST_MODE")) { + ui.m_rememberCrashSituationYes->setChecked(true); + ui.m_reproducibleBox->setCurrentIndex( + s_reproducibleIndex.key(ReportInterface::ReproducibleEverytime)); + } } void BugAwarenessPage::aboutToShow() @@ -159,24 +172,7 @@ { //Save data ReportInterface::Reproducible reproducible = ReportInterface::ReproducibleUnsure; - switch(ui.m_reproducibleBox->currentIndex()) { - case 0: { - reproducible = ReportInterface::ReproducibleUnsure; - break; - } - case 1: { - reproducible = ReportInterface::ReproducibleNever; - break; - } - case 2: { - reproducible = ReportInterface::ReproducibleSometimes; - break; - } - case 3: { - reproducible = ReportInterface::ReproducibleEverytime; - break; - } - } + reproducible = s_reproducibleIndex.value(ui.m_reproducibleBox->currentIndex()); reportInterface()->setBugAwarenessPageData(ui.m_rememberCrashSituationYes->isChecked(), reproducible, diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla.h b/src/bugzillaintegration/reportassistantpages_bugzilla.h --- a/src/bugzillaintegration/reportassistantpages_bugzilla.h +++ b/src/bugzillaintegration/reportassistantpages_bugzilla.h @@ -61,7 +61,6 @@ void updateWidget(bool enabled); bool kWalletEntryExists(const QString&); void openWallet(); - bool canSetCookies(); Ui::AssistantPageBugzillaLogin ui; diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla.cpp b/src/bugzillaintegration/reportassistantpages_bugzilla.cpp --- a/src/bugzillaintegration/reportassistantpages_bugzilla.cpp +++ b/src/bugzillaintegration/reportassistantpages_bugzilla.cpp @@ -111,13 +111,16 @@ void BugzillaLoginPage::bugzillaVersionFound() { + qDebug() << "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"; // Login depends on first knowing the Bugzilla software version number. m_bugzillaVersionFound = true; updateLoginButtonStatus(); } void BugzillaLoginPage::updateLoginButtonStatus() { + qDebug() << "!ui.m_userEdit->text().isEmpty()" << !ui.m_userEdit->text().isEmpty() << "!ui.m_passwordEdit->password().isEmpty()" << !ui.m_passwordEdit->password().isEmpty() + << "m_bugzillaVersionFound" << m_bugzillaVersionFound; ui.m_loginButton->setEnabled( !ui.m_userEdit->text().isEmpty() && !ui.m_passwordEdit->password().isEmpty() && m_bugzillaVersionFound ); @@ -127,7 +130,7 @@ { loginFinished(false); ui.m_statusWidget->setIdle(xi18nc("@info:status","Error when trying to login: " - "%1.", err)); + "%1", err)); if (!extendedMessage.isEmpty()) { new UnhandledErrorDialog(this, err, extendedMessage); } @@ -231,79 +234,9 @@ } } -bool BugzillaLoginPage::canSetCookies() -{ - if (bugzillaManager()->securityMethod() != BugzillaManager::UseCookies) { - qCDebug(DRKONQI_LOG) << "Bugzilla software no longer issues cookies."; - return false; - } - QDBusInterface kded(QStringLiteral("org.kde.kded5"), - QStringLiteral("/kded"), - QStringLiteral("org.kde.kded5")); - QDBusReply kcookiejarLoaded = kded.call(QStringLiteral("loadModule"), - QStringLiteral("kcookiejar")); - if (!kcookiejarLoaded.isValid()) { - KMessageBox::error(this, i18n("Failed to communicate with kded. Make sure it is running.")); - return false; - } else if (!kcookiejarLoaded.value()) { - KMessageBox::error(this, i18n("Failed to load KCookieServer. Check your KDE installation.")); - return false; - } - - - QDBusInterface kcookiejar(QStringLiteral("org.kde.kded5"), - QStringLiteral("/modules/kcookiejar"), - QStringLiteral("org.kde.KCookieServer")); - QDBusReply advice = kcookiejar.call(QStringLiteral("getDomainAdvice"), - KDE_BUGZILLA_URL); - - if (!advice.isValid()) { - KMessageBox::error(this, i18n("Failed to communicate with KCookieServer.")); - return false; - } - - qCDebug(DRKONQI_LOG) << "Got reply from KCookieServer:" << advice.value(); - - if (advice.value() == QLatin1String("Reject")) { - QString msg = i18nc("@info 1 is the bugzilla website url", - "Cookies are not allowed in your KDE network settings. In order to " - "proceed, you need to allow %1 to set cookies.", KDE_BUGZILLA_URL); - - KGuiItem yesItem = KStandardGuiItem::yes(); - yesItem.setText(i18nc("@action:button 1 is the bugzilla website url", - "Allow %1 to set cookies", KDE_BUGZILLA_URL)); - - KGuiItem noItem = KStandardGuiItem::no(); - noItem.setText(i18nc("@action:button do not allow the bugzilla website " - "to set cookies", "No, do not allow")); - - if (KMessageBox::warningYesNo(this, msg, QString(), yesItem, noItem) == KMessageBox::Yes) { - QDBusReply success = kcookiejar.call(QStringLiteral("setDomainAdvice"), - KDE_BUGZILLA_URL, - QStringLiteral("Accept")); - if (!success.isValid() || !success.value()) { - qCWarning(DRKONQI_LOG) << "Failed to set domain advice in KCookieServer"; - return false; - } else { - return true; - } - } else { - return false; - } - } - - return true; -} - void BugzillaLoginPage::loginClicked() { if (!(ui.m_userEdit->text().isEmpty() || ui.m_passwordEdit->password().isEmpty())) { - - if ((bugzillaManager()->securityMethod() == BugzillaManager::UseCookies) - && (!canSetCookies())) { - return; - } - updateWidget(false); if (ui.m_savePasswordCheckBox->checkState()==Qt::Checked) { //Wants to save data diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h --- a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h +++ b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h @@ -64,7 +64,7 @@ bool canSearchMore(); - void searchFinished(const BugMapList&); + void searchFinished(const QList &); void searchError(QString); void analyzedDuplicates(KJob *job); @@ -124,7 +124,7 @@ void cancelAssistant(); private Q_SLOTS: - void bugFetchFinished(BugReport,QObject *); + void bugFetchFinished(Bugzilla::Bug::Ptr, QObject *); void bugFetchError(QString, QObject *); void reloadReport(); diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp --- a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp +++ b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp @@ -44,9 +44,9 @@ resetDates(); connect(bugzillaManager(), &BugzillaManager::searchFinished, - this, &BugzillaDuplicatesPage::searchFinished); - connect(bugzillaManager(), SIGNAL(searchError(QString)), - this, SLOT(searchError(QString))); + this, &BugzillaDuplicatesPage::searchFinished); + connect(bugzillaManager(), &BugzillaManager::searchError, + this, &BugzillaDuplicatesPage::searchError); ui.setupUi(this); ui.information->hide(); @@ -206,38 +206,26 @@ //BEGIN Search related methods void BugzillaDuplicatesPage::searchMore() { - //1 year back - m_searchingEndDate = m_startDate; - m_searchingStartDate = m_searchingEndDate.addYears(-1); + // Start search 6 months before our previous search (or 6 months before now) + m_searchingStartDate = m_startDate.addMonths(-6); performSearch(); } void BugzillaDuplicatesPage::performSearch() { +#warning search needs porting to limit + offset (i.e. pagination) as by-date offsetting is not supported by the service api markAsSearching(true); - QString startDateStr = m_searchingStartDate.toString(QStringLiteral("yyyy-MM-dd")); - QString endDateStr = m_searchingEndDate.toString(QStringLiteral("yyyy-MM-dd")); + QString creationDateStr = m_searchingStartDate.toString(QStringLiteral("yyyy-MM-dd")); - ui.m_statusWidget->setBusy(i18nc("@info:status","Searching for duplicates (from %1 to %2)...", - startDateStr, endDateStr)); + ui.m_statusWidget->setBusy(i18nc("@info:status","Searching for duplicates (crated since %1)...", creationDateStr)); - //Bugzilla will not search on Today bugs if we send the date. - //we need to send "Now" - if (m_searchingEndDate == QDate::currentDate()) { - endDateStr = QLatin1String("Now"); - } - -#if 1 - BugReport report = reportInterface()->newBugReportTemplate(); +#warning fixme why the flip are we generateing a new report template just to get the default severity oO + Bugzilla::NewBug bug = reportInterface()->newBugReportTemplate(); bugzillaManager()->searchBugs(reportInterface()->relatedBugzillaProducts(), - report.bugSeverity(), startDateStr, endDateStr, + bug.severity, creationDateStr, reportInterface()->firstBacktraceFunctions().join(QStringLiteral(" "))); -#else //Test search - bugzillaManager()->searchBugs(QStringList() << "plasma", "crash", startDateStr, endDateStr, - "QGraphicsScenePrivate::processDirtyItemsRecursive"); -#endif } void BugzillaDuplicatesPage::stopCurrentSearch() @@ -287,7 +275,55 @@ return (m_startDate.year() >= 2009); } -void BugzillaDuplicatesPage::searchFinished(const BugMapList & list) +static QString statusString(const Bugzilla::Bug::Ptr &bug) +{ +#warning fixme could move the first switch to if is_open and then switch out NeedsInfo + // Generate a non-geek readable status + switch(bug->status()) { + case Bugzilla::Bug::Status::UNCONFIRMED: + case Bugzilla::Bug::Status::CONFIRMED: + case Bugzilla::Bug::Status::ASSIGNED: + case Bugzilla::Bug::Status::REOPENED: + return i18nc("@info bug status", "[Open]"); + + case Bugzilla::Bug::Status::RESOLVED: + case Bugzilla::Bug::Status::VERIFIED: + case Bugzilla::Bug::Status::CLOSED: + switch(bug->resolution()) { + case Bugzilla::Bug::Resolution::FIXED: + return i18nc("@info bug resolution", "[Fixed]"); + case Bugzilla::Bug::Resolution::WORKSFORME: + return i18nc("@info bug resolution", "[Non-reproducible]"); + case Bugzilla::Bug::Resolution::DUPLICATE: + return i18nc("@info bug resolution", "[Duplicate report]"); + case Bugzilla::Bug::Resolution::INVALID: + return i18nc("@info bug resolution", "[Invalid]"); + case Bugzilla::Bug::Resolution::UPSTREAM: + case Bugzilla::Bug::Resolution::DOWNSTREAM: + return i18nc("@info bug resolution", "[External problem]"); + case Bugzilla::Bug::Resolution::WONTFIX: + case Bugzilla::Bug::Resolution::LATER: + case Bugzilla::Bug::Resolution::REMIND: + case Bugzilla::Bug::Resolution::MOVED: + case Bugzilla::Bug::Resolution::WAITINGFORINFO: + case Bugzilla::Bug::Resolution::BACKTRACE: + case Bugzilla::Bug::Resolution::UNMAINTAINED: + return QString(); + case Bugzilla::Bug::Resolution::Unknown: + Q_UNREACHABLE(); + } + + case Bugzilla::Bug::Status::NEEDSINFO: + return i18nc("@info bug status", "[Incomplete]"); + + case Bugzilla::Bug::Status::Unknown: + Q_UNREACHABLE(); + } + Q_UNREACHABLE(); + QString(); +} + +void BugzillaDuplicatesPage::searchFinished(const QList & list) { KGuiItem::assign(ui.m_searchMoreButton, m_searchMoreGuiItem); m_startDate = m_searchingStartDate; @@ -302,54 +338,23 @@ QList bugIds; for (int i = 0; i < results; i++) { - BugMap bug = list.at(i); - - bool ok; - int bugId = bug.value(QStringLiteral("bug_id")).toInt(&ok); - if (ok) { - bugIds << bugId; - } - - QString title; - - //Generate a non-geek readable status - QString customStatusString; - BugReport::Status status = BugReport::parseStatus(bug.value(QStringLiteral("bug_status"))); - BugReport::Resolution resolution = BugReport::parseResolution(bug.value(QStringLiteral("resolution"))); - if (BugReport::isOpen(status)) { - customStatusString = i18nc("@info bug status", "[Open]"); - } else if (BugReport::isClosed(status) && status != BugReport::NeedsInfo) { - if (resolution == BugReport::Fixed) { - customStatusString = i18nc("@info bug resolution", "[Fixed]"); - } else if (resolution == BugReport::WorksForMe) { - customStatusString = i18nc("@info bug resolution", "[Non-reproducible]"); - } else if (resolution == BugReport::Duplicate) { - customStatusString = i18nc("@info bug resolution", "[Duplicate report]"); - } else if (resolution == BugReport::Invalid) { - customStatusString = i18nc("@info bug resolution", "[Invalid]"); - } else if (resolution == BugReport::Downstream - || resolution == BugReport::Upstream) { - customStatusString = i18nc("@info bug resolution", "[External problem]"); - } - } else if (status == BugReport::NeedsInfo) { - customStatusString = i18nc("@info bug status", "[Incomplete]"); - } + Bugzilla::Bug::Ptr bug = list.at(i); - title = customStatusString + QLatin1Char(' ') + bug[QStringLiteral("short_desc")]; + QString title = statusString(bug) + QLatin1Char(' ') + bug->summary(); - QStringList fields = QStringList() << bug[QStringLiteral("bug_id")] << title; + QStringList fields = QStringList() << QString::number(bug->id()) << title; QTreeWidgetItem * item = new QTreeWidgetItem(fields); - item->setToolTip(0, bug[QStringLiteral("short_desc")]); - item->setToolTip(1, bug[QStringLiteral("short_desc")]); + item->setToolTip(0, bug->summary()); + item->setToolTip(1, bug->summary()); ui.m_bugListWidget->addTopLevelItem(item); } if (!m_foundDuplicate) { markAsSearching(true); DuplicateFinderJob *job = new DuplicateFinderJob(bugIds, bugzillaManager(), this); - connect(job, SIGNAL(result(KJob*)), this, SLOT(analyzedDuplicates(KJob*))); + connect(job, &KJob::result, this, &BugzillaDuplicatesPage::analyzedDuplicates); job->start(); } @@ -380,6 +385,32 @@ } } +static bool isStatusOpen(Bugzilla::Bug::Status status) +{ + switch(status) { + case Bugzilla::Bug::Status::UNCONFIRMED: + case Bugzilla::Bug::Status::CONFIRMED: + case Bugzilla::Bug::Status::ASSIGNED: + case Bugzilla::Bug::Status::REOPENED: + return true; + case Bugzilla::Bug::Status::RESOLVED: + case Bugzilla::Bug::Status::NEEDSINFO: + case Bugzilla::Bug::Status::VERIFIED: + case Bugzilla::Bug::Status::CLOSED: + return false; + + case Bugzilla::Bug::Status::Unknown: + Q_UNREACHABLE(); + } + Q_UNREACHABLE(); + return false; +} + +static bool isStatusClosed(Bugzilla::Bug::Status status) +{ + return !isStatusOpen(status); +} + void BugzillaDuplicatesPage::analyzedDuplicates(KJob *j) { markAsSearching(false); @@ -390,7 +421,7 @@ reportInterface()->setDuplicateId(m_result.parentDuplicate); ui.m_searchMoreButton->setEnabled(!m_foundDuplicate); ui.information->setVisible(m_foundDuplicate); - BugReport::Status status = m_result.status; + auto status = m_result.status; const int duplicate = m_result.duplicate; const int parentDuplicate = m_result.parentDuplicate; @@ -403,14 +434,15 @@ } } +#warning fixme this feels weird. why are we not passing the bug around? QString text; - if (BugReport::isOpen(status) || (BugReport::isClosed(status) && status == BugReport::NeedsInfo)) { + if (isStatusOpen(status) || status == Bugzilla::Bug::Status::NEEDSINFO) { text = (parentDuplicate == duplicate ? i18nc("@label", "Your crash is a duplicate and has already been reported as Bug %1.", QString::number(duplicate)) : - i18nc("@label", "Your crash has already been reported as Bug %1, which is a duplicate of Bug %2", QString::number(duplicate), QString::number(parentDuplicate))) + - QLatin1Char('\n') + i18nc("@label", "Only attach if you can add needed information to the bug report.", QStringLiteral("attach")); - } else if (BugReport::isClosed(status)) { + i18nc("@label", "Your crash has already been reported as Bug %1, which is a duplicate of Bug %2", QString::number(duplicate), QString::number(parentDuplicate))) + + QLatin1Char('\n') + i18nc("@label", "Only attach if you can add needed information to the bug report.", QStringLiteral("attach")); + } else if (isStatusClosed(status)) { text = (parentDuplicate == duplicate ? i18nc("@label", "Your crash has already been reported as Bug %1 which has been closed.", QString::number(duplicate)) : - i18nc("@label", "Your crash has already been reported as Bug %1, which is a duplicate of the closed Bug %2.", QString::number(duplicate), QString::number(parentDuplicate))); + i18nc("@label", "Your crash has already been reported as Bug %1, which is a duplicate of the closed Bug %2.", QString::number(duplicate), QString::number(parentDuplicate))); } ui.information->setText(text); } @@ -650,173 +682,216 @@ show(); } -void BugzillaReportInformationDialog::bugFetchFinished(BugReport report, QObject * jobOwner) +struct Status2 { + QString statusString; + QString closedStateString; +}; + +static Status2 statusString2(const Bugzilla::Bug::Ptr &bug) { - if (jobOwner == this && isVisible()) { - if (report.isValid()) { - - //Handle duplicate state - QString duplicate = report.markedAsDuplicateOf(); - if (!duplicate.isEmpty()) { - bool ok = false; - int dupId = duplicate.toInt(&ok); - if (ok && dupId > 0) { - ui.m_statusWidget->setIdle(QString()); - - KGuiItem yesItem = KStandardGuiItem::yes(); - yesItem.setText(i18nc("@action:button let the user to choose to read the " - "main report", "Yes, read the main report")); - - KGuiItem noItem = KStandardGuiItem::no(); - noItem.setText(i18nc("@action:button let the user choose to read the original " - "report", "No, let me read the report I selected")); - - if (KMessageBox::questionYesNo(this, - xi18nc("@info","The report you selected (bug %1) is already " - "marked as duplicate of bug %2. " - "Do you want to read that report instead? (recommended)", - report.bugNumber(), QString::number(dupId)), - i18nc("@title:window","Nested duplicate detected"), yesItem, noItem) - == KMessageBox::Yes) { - showBugReport(dupId); - return; - } - } + // Generate a non-geek readable status + switch(bug->status()) { + case Bugzilla::Bug::Status::UNCONFIRMED: + return { i18nc("@info bug status", "Opened (Unconfirmed)"), QString() }; + case Bugzilla::Bug::Status::CONFIRMED: + case Bugzilla::Bug::Status::ASSIGNED: + case Bugzilla::Bug::Status::REOPENED: + return { i18nc("@info bug status", "Opened (Unfixed)"), QString() }; + + case Bugzilla::Bug::Status::RESOLVED: + case Bugzilla::Bug::Status::VERIFIED: + case Bugzilla::Bug::Status::CLOSED: + switch(bug->resolution()) { + case Bugzilla::Bug::Resolution::FIXED: { + auto fixedIn = bug->customField("cf_versionfixedin").toString(); + if (!fixedIn.isEmpty()) { + return { i18nc("@info bug resolution, fixed in version", + "Fixed in version \"%1\"", + fixedIn), + i18nc("@info bug resolution, fixed by kde devs in version", + "the bug was fixed by KDE developers in version \"%1\"", + fixedIn) + }; } + return { + i18nc("@info bug resolution", "Fixed"), + i18nc("@info bug resolution", "the bug was fixed by KDE developers") + }; + } - //Generate html for comments (with proper numbering) - QLatin1String duplicatesMark = QLatin1String("has been marked as a duplicate of this bug."); - - QString comments; - QStringList commentList = report.comments(); - for (int i = 0; i < commentList.count(); i++) { - QString comment = commentList.at(i); - //Don't add duplicates mark comments - if (!comment.contains(duplicatesMark)) { - comment.replace(QLatin1Char('\n'), QLatin1String("
")); - comments += i18nc("comment $number to use as subtitle", "

Comment %1:

", (i+1)) - + QStringLiteral("

") + comment + QStringLiteral("


"); - //Count the inline attached crashes (DrKonqi feature) - QLatin1String attachedCrashMark = - QLatin1String("New crash information added by DrKonqi"); - if (comment.contains(attachedCrashMark)) { - m_duplicatesCount++; - } - } else { - //Count duplicate - m_duplicatesCount++; - } - } + case Bugzilla::Bug::Resolution::WORKSFORME: + return { i18nc("@info bug resolution", "Non-reproducible"), QString() }; + case Bugzilla::Bug::Resolution::DUPLICATE: + return { i18nc("@info bug resolution", "Duplicate report (Already reported before)"), QString() }; + case Bugzilla::Bug::Resolution::INVALID: + return { i18nc("@info bug resolution", "Not a valid report/crash"), QString() }; + case Bugzilla::Bug::Resolution::UPSTREAM: + case Bugzilla::Bug::Resolution::DOWNSTREAM: + return { i18nc("@info bug resolution", "Not caused by a problem in the KDE's Applications or libraries"), + i18nc("@info bug resolution", "the bug is caused by a problem in an external application or library, or by a distribution or packaging issue") }; + case Bugzilla::Bug::Resolution::WONTFIX: + case Bugzilla::Bug::Resolution::LATER: + case Bugzilla::Bug::Resolution::REMIND: + case Bugzilla::Bug::Resolution::MOVED: + case Bugzilla::Bug::Resolution::WAITINGFORINFO: + case Bugzilla::Bug::Resolution::BACKTRACE: + case Bugzilla::Bug::Resolution::UNMAINTAINED: + return { QVariant::fromValue(bug->resolution()).toString(), QString() }; + case Bugzilla::Bug::Resolution::Unknown: + Q_UNREACHABLE(); + } + Q_UNREACHABLE(); + return {}; - //Generate a non-geek readable status - QString customStatusString; - BugReport::Status status = report.statusValue(); - BugReport::Resolution resolution = report.resolutionValue(); - if (status == BugReport::Unconfirmed) { - customStatusString = i18nc("@info bug status", "Opened (Unconfirmed)"); - } else if (report.isOpen()) { - customStatusString = i18nc("@info bug status", "Opened (Unfixed)"); - } else if (report.isClosed() && status != BugReport::NeedsInfo) { - QString customResolutionString; - if (resolution == BugReport::Fixed) { - if (!report.versionFixedIn().isEmpty()) { - customResolutionString = i18nc("@info bug resolution, fixed in version", - "Fixed in version \"%1\"", - report.versionFixedIn()); - m_closedStateString = i18nc("@info bug resolution, fixed by kde devs in version", - "the bug was fixed by KDE developers in version \"%1\"", - report.versionFixedIn()); - } else { - customResolutionString = i18nc("@info bug resolution", "Fixed"); - m_closedStateString = i18nc("@info bug resolution", "the bug was fixed by KDE developers"); - } - } else if (resolution == BugReport::WorksForMe) { - customResolutionString = i18nc("@info bug resolution", "Non-reproducible"); - } else if (resolution == BugReport::Duplicate) { - customResolutionString = i18nc("@info bug resolution", "Duplicate report " - "(Already reported before)"); - } else if (resolution == BugReport::Invalid) { - customResolutionString = i18nc("@info bug resolution", "Not a valid report/crash"); - } else if (resolution == BugReport::Downstream || resolution == BugReport::Upstream) { - customResolutionString = i18nc("@info bug resolution", "Not caused by a problem " - "in the KDE's Applications or libraries"); - m_closedStateString = i18nc("@info bug resolution", "the bug is caused by a " - "problem in an external application or library, or " - "by a distribution or packaging issue"); - } else { - customResolutionString = report.resolution(); - } - - customStatusString = i18nc("@info bug status, %1 is the resolution", "Closed (%1)", - customResolutionString); - } else if (status == BugReport::NeedsInfo) { - customStatusString = i18nc("@info bug status", "Temporarily closed, because of a lack " - "of information"); - } else { //Fallback to other raw values - customStatusString = QStringLiteral("%1 (%2)").arg(report.bugStatus(), report.resolution()); - } + case Bugzilla::Bug::Status::NEEDSINFO: + return { i18nc("@info bug status", "Temporarily closed, because of a lack of information"), QString() }; - //Generate notes - QString notes = xi18n("

The bug report's title is often written by its reporter " - "and may not reflect the bug's nature, root cause or other visible " - "symptoms you could use to compare to your crash. Please read the " - "complete report and all the comments below.

"); - - if (m_duplicatesCount >= 10) { //Consider a possible mass duplicate crash - notes += xi18np("

This bug report has %1 duplicate report. That means this " - "is probably a common crash. Please consider only " - "adding a comment or a note if you can provide new valuable " - "information which was not already mentioned.

", - "

This bug report has %1 duplicate reports. That means this " - "is probably a common crash. Please consider only " - "adding a comment or a note if you can provide new valuable " - "information which was not already mentioned.

", - m_duplicatesCount); - } + case Bugzilla::Bug::Status::Unknown: + Q_UNREACHABLE(); - //A manually entered bug ID could represent a normal bug - if (report.bugSeverity() != QLatin1String("crash") - && report.bugSeverity() != QLatin1String("major") - && report.bugSeverity() != QLatin1String("grave") - && report.bugSeverity() != QLatin1String("critical")) - { - notes += xi18n("

This bug report is not about a crash or about any other " - "critical bug.

"); - } +#warning fixme what to do with this +// } else { //Fallback to other raw values +// customStatusString = QStringLiteral("%1 (%2)").arg(report->status(), report->resolution()); +// } + } + Q_UNREACHABLE(); + return {}; +} + +void BugzillaReportInformationDialog::bugFetchFinished(Bugzilla::Bug::Ptr report, QObject * jobOwner) +{ +#warning off what the fuck eh jobowner what how why huh? this is killing me + if (jobOwner != this || !isVisible()) { + return; + } + + if (!report) { + bugFetchError(i18nc("@info", "Invalid report information (malformed data). This could " + "mean that the bug report does not exist, or the bug tracking site " + "is experiencing a problem."), this); + return; + } - //Generate HTML text - QString text = - i18nc("@info bug report title (quoted)", - "

\"%1\"

", report.shortDescription()) + - notes + - i18nc("@info bug report status", - "

Bug Report Status: %1

", customStatusString) + - i18nc("@info bug report product and component", - "

Affected Component: %1 (%2)

", - report.product(), report.component()) + - i18nc("@info bug report description", - "

Description of the bug

%1

", - report.description().replace(QLatin1Char('\n'), QLatin1String("
"))); - - if (!comments.isEmpty()) { - text += i18nc("@label:textbox bug report comments (already formatted)", - "

Additional Comments

%1", comments); - } - ui.m_infoBrowser->setText(text); - ui.m_infoBrowser->setEnabled(true); + // Handle duplicate state + if (report->dupe_of() > 0) { + ui.m_statusWidget->setIdle(QString()); + + KGuiItem yesItem = KStandardGuiItem::yes(); + yesItem.setText(i18nc("@action:button let the user to choose to read the " + "main report", "Yes, read the main report")); + + KGuiItem noItem = KStandardGuiItem::no(); + noItem.setText(i18nc("@action:button let the user choose to read the original " + "report", "No, let me read the report I selected")); + + auto ret = KMessageBox::questionYesNo( + this, + xi18nc("@info","The report you selected (bug %1) is already " + "marked as duplicate of bug %2. " + "Do you want to read that report instead? (recommended)", + report->id(), QString::number(report->dupe_of())), + i18nc("@title:window","Nested duplicate detected"), + yesItem, + noItem); + if (ret == KMessageBox::Yes) { + qDebug() << "REDIRECT"; + showBugReport(report->dupe_of()); + return; + } + } - m_suggestButton->setEnabled(m_relatedButtonEnabled); - m_suggestButton->setVisible(m_relatedButtonEnabled); + // Generate html for comments (with proper numbering) + QLatin1String duplicatesMark = QLatin1String("has been marked as a duplicate of this bug."); - ui.m_statusWidget->setIdle(xi18nc("@info:status", "Showing bug %1", - QString::number(report.bugNumberAsInt()))); + QString comments; +#warning fixme comments arent very oop + QString description; // aka first comment + auto commentsList = report->comments(); + if (commentsList.size() > 0) { + description = commentsList.takeFirst()->text(); + } + for (auto it = commentsList.constBegin(); it != commentsList.constEnd(); ++it) { + QString comment = (*it)->text(); + //Don't add duplicates mark comments + if (!comment.contains(duplicatesMark)) { + comment.replace(QLatin1Char('\n'), QLatin1String("
")); + const int i = it - commentsList.constBegin(); + comments += i18nc("comment $number to use as subtitle", "

Comment %1:

", (i+1)) + + QStringLiteral("

") + comment + QStringLiteral("


"); + //Count the inline attached crashes (DrKonqi feature) + QLatin1String attachedCrashMark = + QLatin1String("New crash information added by DrKonqi"); + if (comment.contains(attachedCrashMark)) { + m_duplicatesCount++; + } } else { - bugFetchError(i18nc("@info", "Invalid report information (malformed data). This could " - "mean that the bug report does not exist, or the bug tracking site " - "is experiencing a problem."), this); + //Count duplicate + m_duplicatesCount++; } } + + //Generate a non-geek readable status + auto str = statusString2(report); + QString customStatusString = str.statusString; + m_closedStateString = str.closedStateString; + + //Generate notes + QString notes = xi18n("

The bug report's title is often written by its reporter " + "and may not reflect the bug's nature, root cause or other visible " + "symptoms you could use to compare to your crash. Please read the " + "complete report and all the comments below.

"); + + if (m_duplicatesCount >= 10) { //Consider a possible mass duplicate crash + notes += xi18np("

This bug report has %1 duplicate report. That means this " + "is probably a common crash. Please consider only " + "adding a comment or a note if you can provide new valuable " + "information which was not already mentioned.

", + "

This bug report has %1 duplicate reports. That means this " + "is probably a common crash. Please consider only " + "adding a comment or a note if you can provide new valuable " + "information which was not already mentioned.

", + m_duplicatesCount); + } + + //A manually entered bug ID could represent a normal bug + if (report->severity() != QLatin1String("crash") + && report->severity() != QLatin1String("major") + && report->severity() != QLatin1String("grave") + && report->severity() != QLatin1String("critical")) + { + notes += xi18n("

This bug report is not about a crash or about any other " + "critical bug.

"); + } + + //Generate HTML text + QString text = + i18nc("@info bug report title (quoted)", + "

\"%1\"

", report->summary()) + + notes + + i18nc("@info bug report status", + "

Bug Report Status: %1

", customStatusString) + + i18nc("@info bug report product and component", + "

Affected Component: %1 (%2)

", + report->product(), + report->component()) + + i18nc("@info bug report description", + "

Description of the bug

%1

", + description.replace(QLatin1Char('\n'), QLatin1String("
"))); + + if (!comments.isEmpty()) { + text += i18nc("@label:textbox bug report comments (already formatted)", + "

Additional Comments

%1", comments); + } + + ui.m_infoBrowser->setText(text); + ui.m_infoBrowser->setEnabled(true); + + m_suggestButton->setEnabled(m_relatedButtonEnabled); + m_suggestButton->setVisible(m_relatedButtonEnabled); + + ui.m_statusWidget->setIdle(xi18nc("@info:status", "Showing bug %1", + QString::number(report->id()))); } void BugzillaReportInformationDialog::markAsDuplicate() diff --git a/src/bugzillaintegration/reportinterface.h b/src/bugzillaintegration/reportinterface.h --- a/src/bugzillaintegration/reportinterface.h +++ b/src/bugzillaintegration/reportinterface.h @@ -24,7 +24,9 @@ #include #include -class BugReport; +namespace Bugzilla { +struct NewBug; +} class BugzillaManager; class ProductMapping; @@ -58,7 +60,7 @@ QString generateReportFullText(bool drKonqiStamp) const; - BugReport newBugReportTemplate() const; + Bugzilla::NewBug newBugReportTemplate() const; void sendBugReport() const; QStringList relatedBugzillaProducts() const; diff --git a/src/bugzillaintegration/reportinterface.cpp b/src/bugzillaintegration/reportinterface.cpp --- a/src/bugzillaintegration/reportinterface.cpp +++ b/src/bugzillaintegration/reportinterface.cpp @@ -261,44 +261,26 @@ return comment; } -BugReport ReportInterface::newBugReportTemplate() const +Bugzilla::NewBug ReportInterface::newBugReportTemplate() const { - //Generate a new bug report template with some values on it - BugReport report; - const SystemInformation * sysInfo = DrKonqi::systemInformation(); - report.setProduct(m_productMapping->bugzillaProduct()); - report.setComponent(m_productMapping->bugzillaComponent()); - report.setVersion(m_productMapping->bugzillaVersion()); - report.setOperatingSystem(sysInfo->bugzillaOperatingSystem()); + Bugzilla::NewBug bug; + bug.product = m_productMapping->bugzillaProduct(); + bug.component = m_productMapping->bugzillaComponent(); + bug.version = m_productMapping->bugzillaVersion(); + bug.op_sys = sysInfo->bugzillaOperatingSystem(); if (sysInfo->compiledSources()) { - report.setPlatform(QLatin1String("Compiled Sources")); + bug.platform = QLatin1String("Compiled Sources"); } else { - report.setPlatform(sysInfo->bugzillaPlatform()); - } - report.setKeywords(QStringList() << QStringLiteral("drkonqi")); - report.setPriority(QLatin1String("NOR")); - report.setBugSeverity(QLatin1String("crash")); - - /* - Disable the backtrace functions on title for RELEASE. - It also needs a bit of polishment - - QString title = m_reportTitle; - - //If there are not too much possible duplicates by query then there are more possibilities - //that this report is unique. Let's add the backtrace functions to the title - if (m_allPossibleDuplicatesByQuery.count() <= 2) { - if (!m_firstBacktraceFunctions.isEmpty()) { - title += (QLatin1String(" [") + m_firstBacktraceFunctions.join(", ").trimmed() - + QLatin1String("]")); - } + bug.platform = sysInfo->bugzillaPlatform(); } - */ + bug.keywords = QStringList { QStringLiteral("drkonqi") }; + bug.priority = QLatin1String("NOR"); + bug.severity = QLatin1String("crash"); + bug.summary = m_reportTitle; - report.setShortDescription(m_reportTitle); - return report; + return bug; } void ReportInterface::sendBugReport() const @@ -312,9 +294,9 @@ m_bugzillaManager->addMeToCC(m_attachToBugNumber); } else { //Creating a new bug report - BugReport report = newBugReportTemplate(); - report.setDescription(generateReportFullText(true)); - report.setValid(true); + Bugzilla::NewBug report = newBugReportTemplate(); + report.description = generateReportFullText(true); + Q_ASSERT(!report.description.isEmpty()); connect(m_bugzillaManager, &BugzillaManager::sendReportErrorInvalidValues, this, &ReportInterface::sendUsingDefaultProduct); connect(m_bugzillaManager, &BugzillaManager::reportSent, this, &ReportInterface::reportSent); @@ -327,13 +309,12 @@ { //Fallback function: if some of the custom values fail, we need to reset all the fields to the default //(and valid) bugzilla values; and try to resend - BugReport report = newBugReportTemplate(); - report.setProduct(QLatin1String("kde")); - report.setComponent(QLatin1String("general")); - report.setPlatform(QLatin1String("unspecified")); - report.setDescription(generateReportFullText(true)); - report.setValid(true); - m_bugzillaManager->sendReport(report); + Bugzilla::NewBug bug = newBugReportTemplate(); + bug.product = QLatin1String("kde"); + bug.component = QLatin1String("general"); + bug.platform = QLatin1String("unspecified"); + bug.description = generateReportFullText(true); + m_bugzillaManager->sendReport(bug); } void ReportInterface::addedToCC() diff --git a/src/drkonqi.cpp b/src/drkonqi.cpp --- a/src/drkonqi.cpp +++ b/src/drkonqi.cpp @@ -331,7 +331,9 @@ bool DrKonqi::ignoreQuality() { - return qEnvironmentVariableIsSet("DRKONQI_IGNORE_QUALITY"); + static bool ignore = qEnvironmentVariableIsSet("DRKONQI_IGNORE_QUALITY") || + qEnvironmentVariableIsSet("DRKONQI_TEST_MODE"); + return ignore; } const QString &DrKonqi::kdeBugzillaURL() @@ -348,6 +350,9 @@ return url; } - url = QStringLiteral("https://bugs.kde.org/"); - return url; + if (qEnvironmentVariableIsSet("DRKONQI_TEST_MODE")) { + return QStringLiteral("https://bugstest.kde.org/"); + } + + return QStringLiteral("https://bugs.kde.org/"); }