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) @@ -83,4 +85,17 @@ install(FILES drkonqi.categories DESTINATION ${KDE_INSTALL_CONFDIR}) endif() +add_custom_target(coverage) +add_custom_command(TARGET coverage + COMMAND ctest + COMMAND lcov -o ${CMAKE_BINARY_DIR}/lcov.report -c -d ${CMAKE_BINARY_DIR} + COMMAND genhtml -o ${CMAKE_BINARY_DIR}/coverage ${CMAKE_BINARY_DIR}/lcov.report + COMMAND find ${CMAKE_BINARY_DIR} -iname *.gcda | xargs rm -v + DEPENDS test +) +add_custom_target(show-coverage + COMMAND xdg-open ${CMAKE_BINARY_DIR}/coverage/index.html + DEPENDS coverage +) + feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) 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/clients/bugclient.h" +#include "bugzillaintegration/libbugzilla/clients/productclient.h" +#include "bugzillaintegration/libbugzilla/clients/attachmentclient.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,62 @@ 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 &comment, int offset); + 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; } - + void fetchComments(const Bugzilla::Bug::Ptr &bug, QObject *jobOwner); 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 commentsFetched(QList comments, 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 commentsError(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,46 @@ #include "bugzillalib.h" -#include -#include -#include -#include - -#include -#include -#include -#include - -#include -#include +#include "bugzillaintegration/libbugzilla/clients/commentclient.h" +#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('/'))); + Bugzilla::setConnection(new Bugzilla::HTTPConnection(QUrl(m_bugTrackerUrl + "rest"))); // 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. + // I am not sure why this is so weirdly done TBH. Might deserve some looking + // into. + 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) { + try { + QString version = Bugzilla::version(job); + setFeaturesForVersion(version); + emit bugzillaVersionFound(); + } catch (Bugzilla::Exception &e) { + // Version detection problems simply mean we'll not mark the version + // found and the UI will not allow reporting. + qDebug() << e.whatString(); + } + }); } void BugzillaManager::setFeaturesForVersion(const QString& version) @@ -101,72 +88,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::Exception &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 +124,149 @@ { return m_username; } -//END Login methods -//BEGIN Bugzilla Action methods -void BugzillaManager::fetchBugReport(int bugnumber, QObject * jobOwner) +void BugzillaManager::fetchBugReport(int bugnumber, QObject *jobOwner) { - QUrl url(m_bugTrackerUrl + QString::fromLatin1(fetchBugUrl).arg(bugnumber)); - - if (!jobOwner) { - jobOwner = this; - } + Bugzilla::BugSearch search; + search.id = bugnumber; - KIO::Job * fetchBugJob = KIO::storedGet(url, KIO::Reload, KIO::HideProgressInfo); - fetchBugJob->setParent(jobOwner); - connect(fetchBugJob, &KIO::Job::finished, this, &BugzillaManager::fetchBugJobFinished); + Bugzilla::BugClient client; + auto job = m_searchJob = client.search(search); + connect(job, &KJob::finished, this, [this, &client, jobOwner](KJob *job) { + try { + auto list = client.search(job); + if (list.size() != 1) { + throw Bugzilla::RuntimeException(QStringLiteral("Unexpected bug amount returned: %1").arg(list.size())); + } + auto bug = list.at(0); + m_searchJob = nullptr; + emit bugReportFetched(bug, jobOwner); + } catch (Bugzilla::Exception &e) { + emit bugReportError(e.whatString(), jobOwner); + } + }); } - -void BugzillaManager::searchBugs(const QStringList & products, - const QString & severity, const QString & date_start, - const QString & date_end, QString comment) +void BugzillaManager::fetchComments(const Bugzilla::Bug::Ptr &bug, QObject *jobOwner) { - 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); + Bugzilla::CommentClient client; + auto job = client.getFromBug2(bug->id()); + job->onResult(this, [this, &client, jobOwner](QList comments){ + try { + qDebug() << "comments arrived" << comments.size(); + emit commentsFetched(comments, jobOwner); + } catch (Bugzilla::Exception &e) { + emit commentsError(e.whatString(), jobOwner); } - } - - QString url = QString(m_bugTrackerUrl) + - QString::fromLatin1(searchUrl).arg(product, comment.replace(QLatin1Char(' ') , QLatin1Char('+')), date_start, - date_end, severity, QString::fromLatin1(columns)); + }); +} + +// TODO: This would kinda benefit from an actual pagination class, +// currently this implicitly relies on the caller to handle offests correctly. +// Fortunately we only have one caller so it makes no difference. +void BugzillaManager::searchBugs(const QStringList &products, + const QString &severity, + const QString &comment, + int offset) +{ + Bugzilla::BugSearch search; + search.products = products; + search.severity = severity; + search.longdesc = comment; + // Order descedingly by bug_id. This allows us to offset through the results + // from newest to oldest. + // The UI will later order our data anyway, so the order at which we receive + // the data is not important for the UI (outside the fact that we want + // to step through pages of data) + search.order << "bug_id DESC"; + search.limit = 25; + search.offset = offset; 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::Exception &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::Exception &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::Exception &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, [this](KJob *job) { + try { + const auto bugId = Bugzilla::BugClient().update(job); + Q_ASSERT(bugId != 0); + emit addMeToCCFinished(bugId); + } catch (Bugzilla::Exception &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::Exception &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 +280,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,21 +47,20 @@ * @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); + DuplicateFinderJob(const QList &bugs, BugzillaManager *manager, QObject *parent = nullptr); ~DuplicateFinderJob() override; void start() override; @@ -80,16 +72,21 @@ Result result() const; private Q_SLOTS: - void slotBugReportFetched(const BugReport &bug, QObject *owner); - void slotBugReportError(const QString &message, QObject *owner); + void slotBugReportFetched(const Bugzilla::Bug::Ptr &bug, QObject *owner); + void slotCommentsFetched(const QList &comments, QObject *owner); + + void slotError(const QString &message, QObject *owner); private: void analyzeNextBug(); - void fetchBug(const QString &bugId); + void fetchBug(int bugId); private: BugzillaManager *m_manager = nullptr; - QList m_bugIds; Result m_result; + + Bugzilla::Bug::Ptr m_bug = nullptr; + + QList m_bugs; }; #endif diff --git a/src/bugzillaintegration/duplicatefinderjob.cpp b/src/bugzillaintegration/duplicatefinderjob.cpp --- a/src/bugzillaintegration/duplicatefinderjob.cpp +++ b/src/bugzillaintegration/duplicatefinderjob.cpp @@ -27,14 +27,21 @@ #include "drkonqi.h" #include "parsebugbacktraces.h" -DuplicateFinderJob::DuplicateFinderJob(const QList &bugIds, BugzillaManager *manager, QObject *parent) +DuplicateFinderJob::DuplicateFinderJob(const QList &bugs, BugzillaManager *manager, QObject *parent) : KJob(parent), m_manager(manager), - m_bugIds(bugIds) + m_bugs(bugs) { - qCDebug(DRKONQI_LOG) << "Possible duplicates:" << m_bugIds; - connect(m_manager, &BugzillaManager::bugReportFetched, this, &DuplicateFinderJob::slotBugReportFetched); - connect(m_manager, &BugzillaManager::bugReportError, this, &DuplicateFinderJob::slotBugReportError); + qCDebug(DRKONQI_LOG) << "Possible duplicates:" << m_bugs.size(); + connect(m_manager, &BugzillaManager::bugReportFetched, + this, &DuplicateFinderJob::slotBugReportFetched); + connect(m_manager, &BugzillaManager::bugReportError, + this, &DuplicateFinderJob::slotError); + + connect(m_manager, &BugzillaManager::commentsFetched, + this, &DuplicateFinderJob::slotCommentsFetched); + connect(m_manager, &BugzillaManager::commentsError, + this, &DuplicateFinderJob::slotError); } DuplicateFinderJob::~DuplicateFinderJob() @@ -53,78 +60,99 @@ void DuplicateFinderJob::analyzeNextBug() { - if (m_bugIds.isEmpty()) { + if (m_bugs.isEmpty()) { emitResult(); return; } - const int bugId = m_bugIds.takeFirst(); - qCDebug(DRKONQI_LOG) << "Fetching:" << bugId; - m_manager->fetchBugReport(bugId, this); + m_bug = m_bugs.takeFirst(); + qCDebug(DRKONQI_LOG) << "Fetching:" << m_bug->id(); + m_manager->fetchComments(m_bug, 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; } - ParseBugBacktraces parse(bug, this); + m_bug = bug; + qCDebug(DRKONQI_LOG) << "Fetching:" << m_bug->id(); + m_manager->fetchComments(m_bug, this); +} + +void DuplicateFinderJob::slotCommentsFetched(const QList &comments, QObject *owner) +{ + if (this != owner) { + return; + } + + // NOTE: we do not hold the comments in our bug object, once they go out + // of scope they are gone again. We have no use for keeping them in memory + // a user might look at 3 out of 20 bugs, and for those we can simply + // request the comments again instead of holding the potentially very large + // comments in memory. + + ParseBugBacktraces parse(comments, this); parse.parse(); BacktraceGenerator *btGenerator = DrKonqi::debuggerManager()->backtraceGenerator(); const ParseBugBacktraces::DuplicateRating rating = parse.findDuplicate(btGenerator->parser()->parsedBacktraceLines()); qCDebug(DRKONQI_LOG) << "Duplicate rating:" << rating; //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" << m_bug->id() << "most likely not a duplicate:" << rating; analyzeNextBug(); return; } + bool unknownStatus = (m_bug->status() == Bugzilla::Bug::Status::Unknown); + bool unknownResolution = (m_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 (m_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 = m_bug->id(); } - fetchBug(bug.markedAsDuplicateOf()); - } else if ((bug.statusValue() == BugReport::UnknownStatus) || (bug.resolutionValue() == BugReport::UnknownResolution)) { + fetchBug(m_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 \"" << m_bug->status() << "\" known:" << !unknownStatus; + qCDebug(DRKONQI_LOG) << "Resolution \"" << m_bug->resolution() << "\" known:" << !unknownResolution; analyzeNextBug(); } else { if (!m_result.duplicate) { - m_result.duplicate = bug.bugNumberAsInt(); + m_result.duplicate = m_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 = m_bug->id(); + m_result.status = m_bug->status(); + m_result.resolution = m_bug->resolution(); + qCDebug(DRKONQI_LOG) << "Found duplicate information (id/status/resolution):" + << m_bug->id() << m_bug->status() << m_bug->resolution(); emitResult(); } } -void DuplicateFinderJob::slotBugReportError(const QString &message, QObject *owner) +void DuplicateFinderJob::slotError(const QString &message, QObject *owner) { if (this != owner) { return; } qCDebug(DRKONQI_LOG) << "Error fetching bug:" << message; analyzeNextBug(); } + 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,42 @@ +# 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 + apijob.cpp + apireply.cpp + bugzilla.cpp + connection.cpp + exceptions.cpp + + clients/attachmentclient.cpp + clients/bugclient.cpp + clients/commentclient.cpp + clients/clientbase.cpp + clients/productclient.cpp + + clients/commands/bugsearch.cpp + clients/commands/bugupdate.cpp + clients/commands/jsoncommand.cpp + clients/commands/newattachment.cpp + clients/commands/newbug.cpp + clients/commands/querycommand.cpp + + models/attachment.cpp + models/bug.cpp + models/comment.cpp + models/logindetails.cpp + models/product.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/apijob.h b/src/bugzillaintegration/libbugzilla/apijob.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/apijob.h @@ -0,0 +1,90 @@ +/* + 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 APIJOB_H +#define APIJOB_H + +#include // Not used here but included so clients don't have to +#include +#include + +#include + +namespace KIO { +class TransferJob; +} + +namespace Bugzilla { + +class APIJob : public KJob +{ + Q_OBJECT +public: + using KJob::KJob; + + /** + * @throws ProtocolException on unexpected HTTP statuses or KIO errors + * @throws APIException when the API returned an error object + * @return json document returned by api request + */ + QJsonDocument document() const; + + /** + * This is a convenience function on top of document() and may throw all + * the same exceptions. + * @return json object of document (may be in valid if the doc has none) + */ + QJsonObject object() const; + + void setAutoStart(bool start); + +public: + // 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; + + bool m_autostart = true; +}; + +class TransferAPIJob : public APIJob +{ + Q_OBJECT + friend class HTTPConnection; // constructs us, ctor is private though +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; +}; + +} // namespace Bugzilla + +#endif // APIJOB_H diff --git a/src/bugzillaintegration/libbugzilla/apijob.cpp b/src/bugzillaintegration/libbugzilla/apijob.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/apijob.cpp @@ -0,0 +1,128 @@ +/* + 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 "apijob.h" + +#include +#include + +#include + +#include "exceptions.h" + +namespace Bugzilla { + +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) { + // Set errors, they are read by document() when the consumer reads + // the data and possibly raised as exception. + setError(job->error()); + setErrorText(job->errorText()); + + 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; +} + +QJsonObject APIJob::object() const +{ + return document().object(); +} + +void APIJob::setAutoStart(bool start) +{ + m_autostart = start; +} + +void APIJob::connectNotify(const QMetaMethod &signal) +{ + if (m_autostart && signal == QMetaMethod::fromSignal(&KJob::finished)) { + qDebug() << "auto starting"; + start(); + } + KJob::connectNotify(signal); +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/apireply.h b/src/bugzillaintegration/libbugzilla/apireply.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/apireply.h @@ -0,0 +1,112 @@ +/* + 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 APIREPLY_H +#define APIREPLY_H + +#include + +#include "apijob.h" + +namespace Bugzilla { + +/** + * Unused. To be evaluated. + * Alternative approach I tried. This technically is nicer because unlike + * KJob we'd not have to call two methods, but instead call the api method and + * set a slot function/lambda on the APIReply. The api method would set a + * "constructor" function that constructs the model type T out of the returned + * json blob. The APIReply would then run and do the http dance, call the + * constructor with the blob and then the slot with the constructed T. + * The huge trouble with this is that in this set up the contstructor needs to + * handle exceptions but it comes from the api method, so it doesn't know what + * to do with exceptions. Exceptions would need handling by the caller, i.e. + * the object setting the slot. So, we cannot really handle exceptions properly + * and we cannot really let the exceptions propagate either because raising + * exceptions out of a slot is considered undefined behavior, and ultimately + * all this still happens inside a slot handler! + * A possible fix would be to have a separate onError and let the result handler + * catch all errors and then forward them to a handler set by the caller. + * This, I fear, is again a bit wonky donky nonesense that doesn't look like + * C++ + * + * e.g. + * + * reply->onResult(this, [=](QList comments) {}); + * reply->onError(this, [=](Exception &exception) {}); + * + * Disadvantage with that is also that it loses the specific capture type. + */ +template +class APIReply : public QObject +{ +public: + APIReply(APIJob *job) + : m_job(job) + { + m_job->setAutoStart(false); + } + + APIReply(const APIReply &other) + : QObject(other.parent()) + , m_job(other.m_job) + , m_context(other.m_context) + , m_slot(other.m_slot) + , m_constructor(other.m_constructor) + { + } + + inline void onResult(QObject *context, std::function slot) + { + m_context = context; + m_slot = slot; + + Q_ASSERT(m_constructor); + // Make sure we life no longer than context and also run in the same + // thread. Part of the callchain happens outside signal-slot protection + // of context so parenting must absolutely be on point. + setParent(context); + + QObject::connect(m_job, &KJob::result, this, [this](KJob *job) { + T construct = m_constructor(job); + m_slot(construct); + deleteLater(); + }); + +#warning fixme is this a problem why did I have start at all. just because kjob has it pure virtual maybe +// m_job->start(); + } + + template + inline void constructResult(Func constructor) + { + m_constructor = constructor; + } + +private: + APIJob *m_job = nullptr; + const QObject *m_context = nullptr; + std::function m_slot = nullptr; + std::function m_constructor = nullptr; +}; + +} // namespace Bugzilla + +#endif // APIREPLY_H diff --git a/src/bugzillaintegration/libbugzilla/apireply.cpp b/src/bugzillaintegration/libbugzilla/apireply.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/apireply.cpp @@ -0,0 +1,21 @@ +/* + 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 "apireply.h" 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,35 @@ +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( + attachmenttest.cpp + bugtest.cpp + bugzillatest.cpp + commenttest.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/attachmenttest.cpp b/src/bugzillaintegration/libbugzilla/autotests/attachmenttest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/attachmenttest.cpp @@ -0,0 +1,131 @@ +/* + 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 + +#include + +#include "../clients/attachmentclient.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 + { + 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 + { + if (path == "/bug/1/attachment") { + QJsonParseError error; + QJsonDocument::fromJson(data, &error); + Q_ASSERT(error.error == QJsonParseError::NoError); + return new JobDouble { QFINDTESTDATA("data/attachment.new.json") }; + } + Q_ASSERT_X(false, "post", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *put(const QString &path, + const QByteArray &, + const QUrlQuery &query = QUrlQuery()) const override + { + Q_ASSERT_X(false, "put", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + } +}; + +class AttachmentTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testCreate() + { + Bugzilla::AttachmentClient c; + NewAttachment attachment; + attachment.ids = { 1 }; + attachment.data = "123"; + attachment.file_name = "filename123"; + attachment.summary = "summary123"; +// attachment.content_type = "123"; + attachment.comment = "comment123"; + attachment.is_patch = true; + attachment.is_private = false; + auto job = c.createAttachment(1, attachment); + job->start(); + QList ids = c.createAttachment(job); + QCOMPARE(ids, QList({1234})); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::AttachmentTest) + +#include "attachmenttest.moc" 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,223 @@ +/* + 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 + +#include + +#include "../clients/bugclient.h" +#include "../clients/productclient.h" + +namespace Bugzilla +{ + +static void compareNewBugHash(const QVariantHash &hash, bool *ok) +{ + *ok = false; + QCOMPARE(hash["product"], "aproduct"); + QCOMPARE(hash["component"], "acomp"); + QCOMPARE(hash["summary"], "asummary"); + QCOMPARE(hash["version"], "aversion"); + QCOMPARE(hash["description"], "adescription"); + QCOMPARE(hash["op_sys"], "asys"); + QCOMPARE(hash["platform"], "aplatform"); + QCOMPARE(hash["priority"], "apriority"); + QCOMPARE(hash["severity"], "aseverity"); + QCOMPARE(hash["keywords"], QStringList({ "aword", "anotherword" })); + *ok = true; +} + +static void compareUpdateBugHash(const QVariantHash &hash, bool *ok) +{ + *ok = false; + QCOMPARE(hash["cc"].toHash()["add"].toStringList(), QStringList({ "me@host.com" })); + QCOMPARE(hash["cc"].toHash()["remove"].toStringList(), QStringList({ "you@host.com" })); + *ok = true; +} + +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 &data, + const QUrlQuery &query = QUrlQuery()) const override + { + qDebug() << path << query.toString(); + if (path == "/bug" && query.isEmpty()) { + QJsonParseError e; + auto doc = QJsonDocument::fromJson(data, &e); + Q_ASSERT(e.error == QJsonParseError::NoError); + auto hash = doc.object().toVariantHash(); + bool ok; + compareNewBugHash(hash, &ok); + Q_ASSERT(ok); + + return new JobDouble { QFINDTESTDATA("data/bugs.new.json") }; + } + Q_ASSERT_X(false, "post", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *put(const QString &path, + const QByteArray &data, + const QUrlQuery &query = QUrlQuery()) const override + { + if (path == "/bug/54321" && query.isEmpty()) { + QJsonParseError e; + auto doc = QJsonDocument::fromJson(data, &e); + Q_ASSERT(e.error == QJsonParseError::NoError); + auto hash = doc.object().toVariantHash(); + bool ok; + compareUpdateBugHash(hash, &ok); + Q_ASSERT(ok); + + return new JobDouble { QFINDTESTDATA("data/bugs.update.json") }; + } + Q_ASSERT_X(false, "put", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } +}; + +class BugTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testSearch() + { + Bugzilla::BugSearch 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); + QCOMPARE(bug->dupe_of(), -1); + QCOMPARE(bug->is_open(), false); + QCOMPARE(bug->customField("cf_versionfixedin"), "5.0"); + } + + void testNewBug() + { + Bugzilla::NewBug bug; + + bug.product = "aproduct"; + bug.component = "acomp"; + bug.summary = "asummary"; + bug.version = "aversion"; + bug.description = "adescription"; + bug.op_sys = "asys"; + bug.platform = "aplatform"; + bug.priority = "apriority"; + bug.severity = "aseverity"; + bug.keywords = QStringList { "aword", "anotherword" }; + + auto job = Bugzilla::BugClient().create(bug); + job->start(); + qint64 id = Bugzilla::BugClient().create(job); + QCOMPARE(id, 12345); + } + + void testUpdateBug() + { + Bugzilla::BugUpdate bug; + bug.cc->add << "me@host.com"; + bug.cc->remove << "you@host.com"; + + auto job = Bugzilla::BugClient().update(54321, bug); + job->start(); + qint64 id = Bugzilla::BugClient().update(job); + QCOMPARE(id, 54321); + } + +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,128 @@ +/* + 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 + +#include + +//#include "../apijob.h" +#include "../bugzilla.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 == "/version") { + return new JobDouble { QFINDTESTDATA("data/bugzilla.version.json") }; + } else if (path == "/login" && query.toString() == "login=auser&password=apass&restrict_login=true") { + return new JobDouble { QFINDTESTDATA("data/bugzilla.login.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 + { + Q_ASSERT_X(false, "post", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *put(const QString &path, + const QByteArray &, + const QUrlQuery &query = QUrlQuery()) const override + { + Q_ASSERT_X(false, "put", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } +}; + +} // namespace Bugzilla + +class BugzillaTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testVersion() + { + KJob *job = Bugzilla::version(); + job->start(); + QCOMPARE(Bugzilla::version(job), "5.0.0"); + } + + void testLogin() + { + KJob *job = Bugzilla::login("auser", "apass"); + job->start(); + auto details = Bugzilla::login(job); + QCOMPARE(details.id, 52960); + QCOMPARE(details.token, "52960-aaaaaaaaaa"); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +QTEST_GUILESS_MAIN(BugzillaTest) + +#include "bugzillatest.moc" diff --git a/src/bugzillaintegration/libbugzilla/autotests/commenttest.cpp b/src/bugzillaintegration/libbugzilla/autotests/commenttest.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/commenttest.cpp @@ -0,0 +1,128 @@ +/* + 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 + +#include + +#include "../clients/commentclient.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/407363/comment" && query.toString().isEmpty()) { + return new JobDouble { QFINDTESTDATA("data/comments.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 + { + Q_ASSERT_X(false, "post", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } + + virtual APIJob *put(const QString &path, + const QByteArray &, + const QUrlQuery &query = QUrlQuery()) const override + { + Q_ASSERT_X(false, "put", + qUtf8Printable(QStringLiteral("unmapped: %1; %2").arg(path, query.toString()))); + return nullptr; + } +}; + +class CommentTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + + void initTestCase() + { + Bugzilla::setConnection(m_doubleConnection); + } + + void testSearch() + { + Bugzilla::CommentClient c; + auto job = c.getFromBug(407363); + job->start(); + auto comments = c.getFromBug(job); + QCOMPARE(comments.size(), 3); + + Comment::Ptr uno = comments[0]; + QCOMPARE(uno->bug_id(), 407363); + QCOMPARE(uno->text(), "uno"); + + Comment::Ptr tre = comments[2]; + QCOMPARE(tre->bug_id(), 407363); + QCOMPARE(tre->text(), "tre"); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::CommentTest) + +#include "commenttest.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,189 @@ +/* + 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 +#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() + { + qDebug() << Q_FUNC_INFO; + // 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, + &t, [&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->disconnect(); + 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() + { + qDebug() << Q_FUNC_INFO; + // 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, + &t, [&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->disconnect(); + socket->close(); + return; + }); + + QUrl root("http://localhost"); + root.setPort(t.serverPort()); + HTTPConnection c(root); + auto job = c.get("/hi"); + job->exec(); + QVERIFY_EXCEPTION_THROWN(job->document(), Bugzilla::APIException); + } + + void testPut() + { + qDebug() << Q_FUNC_INFO; + // qhttpserver is still in qt-labs. as a simple solution do some dumb + // http socketing. + QThread thread; + QTcpServer server; + server.moveToThread(&thread); + + QString readBlob; // lambda member essentially + + connect(&server, &QTcpServer::newConnection, + &server, [&server, &readBlob]() { + QCOMPARE(server.thread(), QThread::currentThread()); + QTcpSocket *socket = server.nextPendingConnection(); + connect(socket, &QTcpSocket::readyRead, + [&server, &readBlob, socket] { + readBlob += socket->readAll(); + readBlob.replace("\r\n", "\n"); + auto parts = readBlob.split("\n"); + if (parts.contains("PUT /put HTTP/1.1") && + parts.contains("Content-Length: 12") && + parts.contains("hello there!")) { + 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(); + qDebug() << "socket closed"; + } + }); + }); + + 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!"); + KJob *kjob = job; + QSignalSpy spy(job, &KJob::finished); + kjob->start(); + // Because of how the request handling works the server may never return + // anything, so wait for the reply, if it doesn't arrive something went + // wrong with the server-side handling and the test cannot complete. + QVERIFY(spy.wait()); + + thread.quit(); + thread.wait(); + thread.terminate(); + + QCOMPARE(job->error(), KJob::NoError); + QCOMPARE(job->data(), "General Kenobi!\r\n"); + } +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::ConnectionTest) + +#include "connectiontest.moc" diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/attachment.new.json b/src/bugzillaintegration/libbugzilla/autotests/data/attachment.new.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/attachment.new.json @@ -0,0 +1,3 @@ +{ + "ids": [1234] +} 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" : "5.0", + "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/bugs.new.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.new.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.new.json @@ -0,0 +1,3 @@ +{ + "id": 12345 +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugs.update.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.update.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.update.json @@ -0,0 +1,3 @@ +{ + "id": 54321 +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.login.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.login.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.login.json @@ -0,0 +1,4 @@ +{ + "id" : 52960, + "token" : "52960-aaaaaaaaaa" +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.version.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.version.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.version.json @@ -0,0 +1,3 @@ +{ + "version": "5.0.0" +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/comments.json b/src/bugzillaintegration/libbugzilla/autotests/data/comments.json new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/comments.json @@ -0,0 +1,45 @@ +{ + "bugs" : { + "407363" : { + "comments" : [ + { + "attachment_id" : null, + "bug_id" : 407363, + "count" : 0, + "creation_time" : "2019-05-09T13:49:28Z", + "creator" : "sitter@kde.org", + "id" : 1855155, + "is_private" : null, + "tags" : [], + "text" : "uno", + "time" : "2019-05-09T13:49:28Z" + }, + { + "attachment_id" : 119931, + "bug_id" : 407363, + "count" : 1, + "creation_time" : "2019-05-09T13:50:10Z", + "creator" : "sitter@kde.org", + "id" : 1855156, + "is_private" : null, + "tags" : [], + "text" : "due", + "time" : "2019-05-09T13:50:10Z" + }, + { + "attachment_id" : 119932, + "bug_id" : 407363, + "count" : 2, + "creation_time" : "2019-05-09T13:50:28Z", + "creator" : "sitter@kde.org", + "id" : 1855157, + "is_private" : null, + "tags" : [], + "text" : "tre", + "time" : "2019-05-09T13:50:28Z" + } + ] + } + }, + "comments" : {} +} 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,5 @@ +HTTP/1.1 201 Created +Date: Thu, 07 Mar 2019 12:11:29 GMT +Server: Apache/2.4.29 (Ubuntu) + +General Kenobi! 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,136 @@ +/* + 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 +#include + +#warning fixme should probably use the target somehow so we dont have to do relative includes +#include "../clients/productclient.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 &, + 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); + QCOMPARE(product->componentNames(), QStringList({"general"})); + QCOMPARE(product->allVersions(), + QStringList({"2.0", "2.0-beta1", "2.0-git", "2.0.x", "17.04", + "17.08", "17.12", "18.04", "18.08", "18.12", + "SVN", "unspecified"})); + + QCOMPARE(product->versions().size(), 12); + auto version = product->versions()[0]; + QCOMPARE(version->id(), 4408); + QCOMPARE(version->name(), "2.0"); + QCOMPARE(version->isActive(), false); + + QCOMPARE(product->components().size(), 1); + auto component = product->components()[0]; + QCOMPARE(component->id(), 1200); + QCOMPARE(component->name(), "general"); + } + +private: + Bugzilla::ConnectionDouble *m_doubleConnection = new Bugzilla::ConnectionDouble; +}; + +} // namespace Bugzilla + +QTEST_MAIN(Bugzilla::ProductTest) + +#include "producttest.moc" 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,39 @@ +/* + 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" +#include "models/logindetails.h" + +namespace Bugzilla +{ + 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,56 @@ +/* + 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) +{ + const APIJob *job = qobject_cast(kjob); + const QString version = job->object().value(QLatin1String("version")).toString(); + return version; +} + +APIJob *version(const Connection &connection) +{ + return connection.get(QStringLiteral("/version")); +} + +LoginDetails login(KJob *kjob) +{ + const APIJob *job = qobject_cast(kjob); + const auto obj = job->object(); + const QString token = obj.value(QLatin1String("token")).toString(); + const int id = obj.value(QLatin1String("id")).toInt(-1); + 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/clients/attachmentclient.h b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.h @@ -0,0 +1,42 @@ +/* + 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 ATTACHMENTCLIENT_H +#define ATTACHMENTCLIENT_H + +#include + +#include "clientbase.h" +#include "commands/newattachment.h" + +namespace Bugzilla { + +class AttachmentClient : public ClientBase +{ +public: + using ClientBase::ClientBase; + + QList createAttachment(KJob *kjob); + KJob *createAttachment(int bugId, const NewAttachment &attachment); +}; + +} // namespace Bugzilla + +#endif // ATTACHMENTCLIENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/attachmentclient.cpp b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.cpp @@ -0,0 +1,52 @@ +/* + 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 "attachmentclient.h" + +#include + +namespace Bugzilla { + +QList AttachmentClient::createAttachment(KJob *kjob) +{ + APIJob *job = qobject_cast(kjob); + + auto ary = job->object().value(QStringLiteral("ids")).toArray(); + // It's unclear if this can happen. When the ids would be empty there was + // an error, and when there was an error the API should have sent an error. + Q_ASSERT(ary.size() > 0); + + QList list; + for (auto ids : ary) { + bool ok = false; + list.append(ids.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/clients/bugclient.h b/src/bugzillaintegration/libbugzilla/clients/bugclient.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/bugclient.h @@ -0,0 +1,50 @@ +/* + 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 BUGCLIENT_H +#define BUGCLIENT_H + +#include "clientbase.h" +#include "commands/bugsearch.h" +#include "commands/bugupdate.h" +#include "commands/newbug.h" + +#include + +namespace Bugzilla { + +class BugClient : public ClientBase +{ +public: + using ClientBase::ClientBase; + + QList search(KJob *kjob); + KJob *search(const BugSearch &search); + + qint64 create(KJob *kjob); + KJob *create(const NewBug &bug); + + qint64 update(KJob *kjob); + KJob *update(qint64 bugId, BugUpdate &bug); +}; + +} // namespace Bugzilla + +#endif // BUGCLIENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/bugclient.cpp b/src/bugzillaintegration/libbugzilla/clients/bugclient.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/bugclient.cpp @@ -0,0 +1,77 @@ +/* + 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 "bugclient.h" + +#include +#include + +namespace Bugzilla { + +static qint64 jobToId(KJob *kjob) +{ + APIJob *job = qobject_cast(kjob); + + qint64 ret = job->object().value(QStringLiteral("id")).toInt(-1); + Q_ASSERT(ret != -1); + return ret; +} + +QList BugClient::search(KJob *kjob) +{ + APIJob *job = qobject_cast(kjob); + + auto ary = job->object().value(QStringLiteral("bugs")).toArray(); + + QList list; + for (auto bug : ary) { + list.append(Bug::Ptr(new Bug(bug.toObject().toVariantHash()))); + } + + return list; +} + +KJob *BugClient::search(const BugSearch &search) +{ + return m_connection.get(QStringLiteral("/bug"), search.toQuery()); +} + +qint64 BugClient::create(KJob *kjob) +{ + return jobToId(kjob); +} + +KJob *BugClient::create(const NewBug &bug) +{ + return m_connection.post(QStringLiteral("/bug"), + bug.toJson()); +} + +qint64 BugClient::update(KJob *kjob) +{ + return jobToId(kjob); +} + +KJob *BugClient::update(qint64 bugId, BugUpdate &bug) +{ + return m_connection.put(QStringLiteral("/bug/%1").arg(bugId), bug.toJson()); +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/clientbase.h b/src/bugzillaintegration/libbugzilla/clients/clientbase.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/clientbase.h @@ -0,0 +1,39 @@ +/* + 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 CLIENTBASE_H +#define CLIENTBASE_H + +#include "connection.h" + +namespace Bugzilla { + +class ClientBase +{ +public: + explicit ClientBase(const Connection &connection = Bugzilla::connection()); + +protected: + const Connection &m_connection; +}; + +} // namespace Bugzilla + +#endif // CLIENTBASE_H diff --git a/src/bugzillaintegration/libbugzilla/clients/clientbase.cpp b/src/bugzillaintegration/libbugzilla/clients/clientbase.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/clientbase.cpp @@ -0,0 +1,30 @@ +/* + 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 "clientbase.h" + +namespace Bugzilla { + +ClientBase::ClientBase(const Connection &connection) + : m_connection(connection) +{ +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.h b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.h @@ -0,0 +1,45 @@ +/* + 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 BUGSEARCH_H +#define BUGSEARCH_H + +#include "querycommand.h" + +namespace Bugzilla { + +class BugSearch : public QueryCommand +{ + Q_OBJECT + BUGZILLA_MEMBER_PROPERTY(QStringList, products); + BUGZILLA_MEMBER_PROPERTY(QString, severity); + BUGZILLA_MEMBER_PROPERTY(QString, creationTime); + BUGZILLA_MEMBER_PROPERTY(qint64, id) = -1; + BUGZILLA_MEMBER_PROPERTY(qint64, limit) = -1; + BUGZILLA_MEMBER_PROPERTY(qint64, offset) = -1; + BUGZILLA_MEMBER_PROPERTY(QString, longdesc); + BUGZILLA_MEMBER_PROPERTY(QStringList, order); +public: + virtual QUrlQuery toQuery() const override; +}; + +} // namespace Bugzilla + +#endif // BUGSEARCH_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.cpp @@ -0,0 +1,49 @@ +/* + 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 "bugsearch.h" + +#include + +namespace Bugzilla { + +QUrlQuery BugSearch::toQuery() const +{ + QUrlQuery query; + QSet seen; + + for (const QString &product : products) { + query.addQueryItem(QStringLiteral("product"), product); + } + seen << QStringLiteral("products"); + + if (!order.isEmpty()) { + query.addQueryItem(QStringLiteral("order"), order.join(',')); + } + seen << QStringLiteral("order"); + + expandQuery(query, seen); + + qDebug() << query.toString(); + + return query; +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.h b/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.h @@ -0,0 +1,45 @@ +/* + 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 BUGUPDATE_H +#define BUGUPDATE_H + +#include "jsoncommand.h" + +namespace Bugzilla { + +class BugUpdateCC : public JsonCommand +{ + Q_OBJECT + BUGZILLA_MEMBER_PROPERTY(QStringList, add); + BUGZILLA_MEMBER_PROPERTY(QStringList, remove); +public: + using JsonCommand::JsonCommand; +}; + +class BugUpdate : public JsonCommand +{ + Q_OBJECT + BUGZILLA_MEMBER_PROPERTY(BugUpdateCC *, cc) = new BugUpdateCC(this); +}; + +} // namespace Bugzilla + +#endif // BUGUPDATE_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.cpp @@ -0,0 +1,24 @@ +/* + 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 "bugupdate.h" + +namespace Bugzilla { +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/commandbase.h b/src/bugzillaintegration/libbugzilla/clients/commands/commandbase.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/commandbase.h @@ -0,0 +1,30 @@ +/* + 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 COMMANDBASE_H +#define COMMANDBASE_H + +#define BUGZILLA_MEMBER_PROPERTY(type, member) \ + private: \ + Q_PROPERTY(type member MEMBER member) \ + public: \ + type member + +#endif // COMMANDBASE_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.h b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.h @@ -0,0 +1,44 @@ +/* + 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 JSONCOMMAND_H +#define JSONCOMMAND_H + +#include + +#include "commandbase.h" + +namespace Bugzilla { + +class JsonCommand : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; + + virtual QByteArray toJson() const; + virtual QVariantHash toVariantHash() const; +}; + +} // namespace Bugzilla + +Q_DECLARE_METATYPE(Bugzilla::JsonCommand *) + +#endif // JSONCOMMAND_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.cpp @@ -0,0 +1,70 @@ +/* + 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 "jsoncommand.h" + +#include +#include +#include +#include + +namespace Bugzilla { + +QByteArray JsonCommand::toJson() const +{ + QJsonDocument doc; + doc.setObject(QJsonObject::fromVariantHash(toVariantHash())); + return doc.toJson(); +} + +QVariantHash JsonCommand::toVariantHash() const +{ + QVariantHash hash; + + const auto propertyCount = metaObject()->propertyCount(); + qDebug() << propertyCount; + for (int i = 0; i < propertyCount; ++i) { + const auto property = metaObject()->property(i); + const auto name = QString::fromLatin1(property.name()); + const auto value = property.read(this); + + if (name == "objectName") { // Builtin property. + continue; + } + + if (value.isNull()) { + continue; + } + + // If this is a nested representation, serialize it and glue it in. + if (value.canConvert()) { + JsonCommand *repValue = value.value(); + hash.insert(name, repValue->toVariantHash()); + continue; + } + + hash.insert(name, value); + } + + return hash; +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.h b/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.h @@ -0,0 +1,48 @@ +/* + 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 NEWATTACHMENT_H +#define NEWATTACHMENT_H + +#include "jsoncommand.h" + +namespace Bugzilla { + +class NewAttachment : public JsonCommand +{ + Q_OBJECT + BUGZILLA_MEMBER_PROPERTY(QList, ids); + BUGZILLA_MEMBER_PROPERTY(QString, data); + BUGZILLA_MEMBER_PROPERTY(QString, file_name); + BUGZILLA_MEMBER_PROPERTY(QString, summary); + BUGZILLA_MEMBER_PROPERTY(QString, content_type); + BUGZILLA_MEMBER_PROPERTY(QString, comment); + BUGZILLA_MEMBER_PROPERTY(bool, is_patch); + BUGZILLA_MEMBER_PROPERTY(bool, is_private); + + // flags property is not supported at this time + +public: + virtual QVariantHash toVariantHash() const override; +}; + +} // namespace Bugzilla + +#endif // NEWATTACHMENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.cpp @@ -0,0 +1,67 @@ +/* + 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 "newattachment.h" + +#include +#include + +namespace Bugzilla { + +//QByteArray NewAttachment::toJson() const +//{ +//#warning fixme find out if there isnt 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(); +//} + +QVariantHash NewAttachment::toVariantHash() const +{ + auto hash = JsonCommand::toVariantHash(); + + QVariantList idsVariant; + for (int id : ids) { + idsVariant << QVariant::fromValue(id); + } + hash["ids"] = idsVariant; + + hash["data"] = data.toUtf8().toBase64(); + qDebug() << hash; + return hash; +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/newbug.h b/src/bugzillaintegration/libbugzilla/clients/commands/newbug.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/newbug.h @@ -0,0 +1,48 @@ +/* + 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 NEWBUG_H +#define NEWBUG_H + +#include "jsoncommand.h" + +namespace Bugzilla { + +class NewBug : public JsonCommand +{ + Q_OBJECT + BUGZILLA_MEMBER_PROPERTY(QString, product); + BUGZILLA_MEMBER_PROPERTY(QString, component); + BUGZILLA_MEMBER_PROPERTY(QString, summary); // aka shortdesc + BUGZILLA_MEMBER_PROPERTY(QString, version); + BUGZILLA_MEMBER_PROPERTY(QString, description); + BUGZILLA_MEMBER_PROPERTY(QString, op_sys); + BUGZILLA_MEMBER_PROPERTY(QString, platform); + BUGZILLA_MEMBER_PROPERTY(QString, priority); + BUGZILLA_MEMBER_PROPERTY(QString, severity); + BUGZILLA_MEMBER_PROPERTY(QStringList, keywords); // not documented but also supported +public: + using JsonCommand::JsonCommand; + NewBug(const NewBug &other); +}; + +} // namespace Bugzilla + +#endif // NEWBUG_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/newbug.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/newbug.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/newbug.cpp @@ -0,0 +1,37 @@ +/* + 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 "newbug.h" + +#include + +namespace Bugzilla { + +NewBug::NewBug(const NewBug &other) + : JsonCommand(other.parent()) +{ + const auto propertyCount = staticMetaObject.propertyCount(); + for (int i = 0; i < propertyCount; ++i) { + const auto property = staticMetaObject.property(i); + property.write(this, property.read(&other)); + } +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.h b/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.h @@ -0,0 +1,44 @@ +/* + 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 QUERYCOMMAND_H +#define QUERYCOMMAND_H + +#include +#include +#include +#include + +#include "commandbase.h" + +namespace Bugzilla { + +class QueryCommand : public QObject +{ +public: + using QObject::QObject; + + virtual QUrlQuery toQuery() const; + QUrlQuery expandQuery(QUrlQuery &query, const QSet &seen) const; +}; + +} // namespace Bugzilla + +#endif // QUERYCOMMAND_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.cpp @@ -0,0 +1,72 @@ +/* + 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 "querycommand.h" + +#include +#include +#include +#include + +namespace Bugzilla { + +QUrlQuery QueryCommand::toQuery() const +{ + QUrlQuery query; + return expandQuery(query, QSet()); +} + +QUrlQuery QueryCommand::expandQuery(QUrlQuery &query, const QSet &seen) const +{ + const auto propertyCount = metaObject()->propertyCount(); + for (int i = 0; i < propertyCount; ++i) { + const auto property = metaObject()->property(i); + const auto name = QString::fromLatin1(property.name()); + const auto value = property.read(this); + + if (query.hasQueryItem(name) || seen.contains(name) || name == QLatin1Literal("objectName")) { + // The element was manually set or builtin property. + continue; + } + + if (value.toLongLong() < 0) { + // Invalid value => member was not set! + // This does generally also work for all integers, ulonglong of + // course being the only one that can cause trouble. + continue; + } + + // Lists must be serialized manually. They could have a number of representations. + Q_ASSERT_X(value.type() != QVariant::StringList, Q_FUNC_INFO, + qPrintable(QString("Trying to auto serialize string list %1").arg(name))); + + // Either can't serialize or not set. + if (value.toString().isEmpty()) { + continue; + } + + query.addQueryItem(name, property.read(this).toString()); + } + + return query; +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/clients/commentclient.h b/src/bugzillaintegration/libbugzilla/clients/commentclient.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commentclient.h @@ -0,0 +1,44 @@ +/* + 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 COMMENTCLIENT_H +#define COMMENTCLIENT_H + +#include "apireply.h" // only here for expermeintal getFromBug2 impl +#include "clientbase.h" +#include "models/comment.h" + +namespace Bugzilla { + +class CommentClient : public ClientBase +{ +public: + using ClientBase::ClientBase; + + QList getFromBug(KJob *kjob); + KJob *getFromBug(int bugId); + + APIReply> *getFromBug2(int bugId); +}; + + +} // namespace Bugzilla + +#endif // COMMENTCLIENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/commentclient.cpp b/src/bugzillaintegration/libbugzilla/clients/commentclient.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commentclient.cpp @@ -0,0 +1,79 @@ +/* + 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 "commentclient.h" + +#include + +namespace Bugzilla { + +QList CommentClient::getFromBug(KJob *kjob) +{ + APIJob *job = qobject_cast(kjob); + + QJsonObject bugs = job->object().value(QStringLiteral("bugs")).toObject(); + Q_ASSERT(bugs.keys().size() == 1); + +#warning at 0 is a bit wonky + QJsonObject bug = bugs.value(bugs.keys().at(0)).toObject(); + QJsonArray comments = bug.value(QStringLiteral("comments")).toArray(); + + 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))); +} + +#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? +#warning the problem with apireply is that it can raise in a qt slot that has undefined behavior +APIReply> *CommentClient::getFromBug2(int bugId) +{ + APIJob *job = m_connection.get(QStringLiteral("/bug/%1/comment").arg(QString::number(bugId))); + + auto reply = new APIReply>(job); + reply->constructResult([](KJob *kjob) -> QList { + APIJob *job = qobject_cast(kjob); + +#warning code dupe form getFromBug looks like + QJsonObject bugs = job->object().value(QStringLiteral("bugs")).toObject(); + Q_ASSERT(bugs.keys().size() == 1); + + QJsonObject bug = bugs.value(bugs.keys().at(0)).toObject(); + QJsonArray comments = bug.value(QStringLiteral("comments")).toArray(); + + QList list; + for (auto it = comments.constBegin(); it != comments.constEnd(); ++it) { + list.append(new Comment((*it).toObject().toVariantHash())); + } + + return list; + }); + + return reply; +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/productclient.h b/src/bugzillaintegration/libbugzilla/clients/productclient.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/productclient.h @@ -0,0 +1,41 @@ +/* + 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 PRODUCTCLIENT_H +#define PRODUCTCLIENT_H + +#include "clientbase.h" +#include "models/product.h" + +namespace Bugzilla { + +class ProductClient : public ClientBase +{ +public: + using ClientBase::ClientBase; + + /// Gets a single Product by its name + Product::Ptr get(KJob *kjob); + KJob *get(const QString &idOrName); +}; + +} // namespace Bugzilla + +#endif // PRODUCTCLIENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/productclient.cpp b/src/bugzillaintegration/libbugzilla/clients/productclient.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/productclient.cpp @@ -0,0 +1,46 @@ +/* + 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 "productclient.h" + +#include +#include +#include + +namespace Bugzilla { + +Product::Ptr ProductClient::get(KJob *kjob) +{ + auto job = qobject_cast(kjob); + + const QJsonArray productsArray = job->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/libbugzilla/connection.h b/src/bugzillaintegration/libbugzilla/connection.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/connection.h @@ -0,0 +1,89 @@ +/* + 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 "exceptions.h" +#include "apijob.h" + +namespace Bugzilla { + +class APIJob; + +/** + * Base Connection. Has CRUD-y methods that need implementing in a specific + * connection variant. + */ +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; +}; + +/** + * HTTP Connection. + */ +class HTTPConnection : public Connection +{ + Q_OBJECT +public: + explicit HTTPConnection(const QUrl &root = QUrl("http://bugstest.kde.org/"), 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; +}; + +/** + * @return the "default" global connection instance. This is the instance used + * by all clients unless another one is manually set on the client. + */ +Connection &connection(); + +/** + * Changes the "default" global connection. This generally shouldn't be used + * outside tests, where it is used to inject connection doubles. + */ +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,107 @@ +/* + 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 + +namespace Bugzilla { + +// Static container for global default connection. +// We need a container here because the connection may be anything derived from +// Connection and its effective type may change (e.g. in autotests). +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; +} + +HTTPConnection::HTTPConnection(const QUrl &root, QObject *parent) + : Connection(parent) + , m_root(root) +{ +} + +HTTPConnection::~HTTPConnection() +{ +} + +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; +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/exceptions.h b/src/bugzillaintegration/libbugzilla/exceptions.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/exceptions.h @@ -0,0 +1,112 @@ +/* + 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 EXCEPTIONS_H +#define EXCEPTIONS_H + +#include + +class QJsonDocument; +class QJsonObject; + +namespace KIO { +class TransferJob; +} + +namespace Bugzilla { + +class APIJob; + +/** + * Root class for exceptions. Simply a QException which has the what backed + * by a QString. + */ +class Exception : public QException +{ +public: + using QException::QException; + + virtual QString whatString() const = 0; + virtual const char *what() const noexcept override; +}; + +/** + * Generic runtime exception. + */ +class RuntimeException : public Exception +{ +public: + RuntimeException(const QString &reason); + virtual RuntimeException *clone() const override { return new RuntimeException(*this); } + virtual QString whatString() const override; + +private: + QString m_reason; +}; + +/** + * Translates an API error into an excpetion for easy handling. + * Specifically when the API sends an error object in the body attempting to + * access the JSON blob through one of the conveience accessors + * (e.g. job.object()) will instead raise an exception. + */ +class APIException : public Exception +{ +public: + APIException(const QJsonDocument &document); + APIException(const QJsonObject &object); + 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; +}; + +/** + * Translates an KJob/APIJob error into an excpetion for easy handling. + */ +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; +}; + +} // namespace Bugzilla + +#endif // EXCEPTIONS_H diff --git a/src/bugzillaintegration/libbugzilla/exceptions.cpp b/src/bugzillaintegration/libbugzilla/exceptions.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/exceptions.cpp @@ -0,0 +1,106 @@ +/* + 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 "exceptions.h" + +#include + +#include "apijob.h" + +namespace Bugzilla { + +APIException::APIException(const QJsonDocument &document) + : APIException(document.object()) +{ +} + +APIException::APIException(const QJsonObject &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(); + } +} + +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()); +} + +RuntimeException::RuntimeException(const QString &reason) + : m_reason(reason) +{ +} + +QString RuntimeException::whatString() const +{ + return m_reason; +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/models/attachment.h b/src/bugzillaintegration/libbugzilla/models/attachment.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/attachment.h @@ -0,0 +1,39 @@ +/* + 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 + +namespace Bugzilla { + +class Bug; + +class Attachment : public QObject +{ + Q_OBJECT +public: + explicit Attachment(QObject *parent = nullptr); +}; + +} // namespace Bugzilla + +#endif // ATTACHMENT_H diff --git a/src/bugzillaintegration/libbugzilla/models/attachment.cpp b/src/bugzillaintegration/libbugzilla/models/attachment.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/attachment.cpp @@ -0,0 +1,25 @@ +/* + 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" + +#warning is attachment even modelled or used +namespace Bugzilla { +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/models/bug.h b/src/bugzillaintegration/libbugzilla/models/bug.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/bug.h @@ -0,0 +1,153 @@ +/* + 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 + +#include "comment.h" + +namespace Bugzilla { + +// Models a bugzilla bug. +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) + + // 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); + + 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; +}; + +} // namespace Bugzilla + +#endif // BUG_H diff --git a/src/bugzillaintegration/libbugzilla/models/bug.cpp b/src/bugzillaintegration/libbugzilla/models/bug.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/bug.cpp @@ -0,0 +1,174 @@ +/* + 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 +#include +#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; +} + +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; +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/models/comment.h b/src/bugzillaintegration/libbugzilla/models/comment.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/comment.h @@ -0,0 +1,52 @@ +/* + 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 +#include + +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; +}; + +} // namespace Bugzilla + +#endif // COMMENT_H diff --git a/src/bugzillaintegration/libbugzilla/models/comment.cpp b/src/bugzillaintegration/libbugzilla/models/comment.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/comment.cpp @@ -0,0 +1,55 @@ +/* + 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" + +#include + +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; +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/models/logindetails.h b/src/bugzillaintegration/libbugzilla/models/logindetails.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/logindetails.h @@ -0,0 +1,36 @@ +/* + 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 LOGINDETAILS_H +#define LOGINDETAILS_H + +#include + +namespace Bugzilla { + +struct LoginDetails +{ + int id; + QString token; +}; + +} // namespace Bugzilla + +#endif // LOGINDETAILS_H diff --git a/src/bugzillaintegration/libbugzilla/models/logindetails.cpp b/src/bugzillaintegration/libbugzilla/models/logindetails.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/logindetails.cpp @@ -0,0 +1,24 @@ +/* + 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 "logindetails.h" + +namespace Bugzilla { +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/models/product.h b/src/bugzillaintegration/libbugzilla/models/product.h new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/product.h @@ -0,0 +1,117 @@ +/* + 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 + +#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 components 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(); + + bool isActive() const; + void setActive(bool active); + + QList components() const; + void setComponents(const QList &components); + + QList versions() const; + void setVersions(const QList &versions); + + // Convenience methods to get useful content out of the + QStringList componentNames() const; + QStringList allVersions() const; + QStringList activeVersions() const; + QStringList inactiveVersions() const; + +private: + const Connection &m_connection; + + bool m_active = false; + QList m_components; + QList m_versions; +}; + +} // namespace Bugzilla + + +QDebug operator<<(QDebug dbg, const Bugzilla::Product &product); +QDebug operator<<(QDebug dbg, const Bugzilla::Product::Ptr &product); + +Q_DECLARE_METATYPE(Bugzilla::ProductComponent *) +Q_DECLARE_METATYPE(QList) + +#endif // PRODUCT_H diff --git a/src/bugzillaintegration/libbugzilla/models/product.cpp b/src/bugzillaintegration/libbugzilla/models/product.cpp new file mode 100644 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/product.cpp @@ -0,0 +1,168 @@ +/* + 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 +#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; +} + +QList Product::components() 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; +} + +QStringList Product::componentNames() const +{ + QStringList ret; + for (const auto *component : m_components) { + ret << component->name(); + } + return ret; +} +QStringList Product::allVersions() const +{ + QStringList ret; + for (const auto *version : m_versions) { + ret << version->name(); + } + return ret; +} +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()); + } +} + +} // namespace Bugzilla + +QDebug operator<<(QDebug dbg, const Bugzilla::Product &product) +{ + dbg.nospace() << product.metaObject()->className() << "("; + + auto propertyCount = product.metaObject()->propertyCount(); + for (int i = 0; i < propertyCount; ++i) { + auto property = product.metaObject()->property(i); + + dbg.nospace() << property.name() << "=" << property.read(&product) << "; "; + } + +// const QString body = message.body(); +// QVector pieces = body.splitRef("\r\n", QString::SkipEmptyParts); +// if (pieces.isEmpty()) +// dbg.nospace() << "Message()"; +// else if (pieces.size() == 1) +// dbg.nospace() << "Message(" << pieces.first() << ")"; +// else +// dbg.nospace() << "Message(" << pieces.first() << " ...)"; + return dbg.maybeSpace(); +} + +QDebug operator<<(QDebug dbg, const Bugzilla::Product::Ptr &product) +{ +// dbg.nospace() << *product; + return dbg.nospace() << *product; +} 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 QList &comments, QObject *parent = nullptr); void parse(); @@ -56,7 +56,7 @@ private: BacktraceParser *m_parser = nullptr; - const BugReport m_bug; + const QList m_comments; 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,21 +110,18 @@ } } -ParseBugBacktraces::ParseBugBacktraces(const BugReport &bug, QObject *parent) - : QObject(parent), - m_bug(bug) +ParseBugBacktraces::ParseBugBacktraces(const QList &comments, QObject *parent) + : QObject(parent) + , m_comments(comments) { m_parser = BacktraceParser::newParser(QStringLiteral("gdb"), this); m_parser->connectToGenerator(this); } void ParseBugBacktraces::parse() { - parse(m_bug.description()); - - QStringList comments = m_bug.comments(); - foreach (const QString &comment, comments) { - parse(comment); + for (const auto &comment : m_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,20 @@ #include #include +#include "bugzillaintegration/libbugzilla/clients/productclient.h" + class Product; class BugzillaManager; class CrashedApplication; +/** + * Maps our crashed entity to a bugzilla product/component/version. + */ 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 +48,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->componentNames().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 @@ -57,19 +57,16 @@ private Q_SLOTS: /* Search related methods */ void searchMore(); - void performSearch(); void stopCurrentSearch(); void markAsSearching(bool); bool canSearchMore(); - void searchFinished(const BugMapList&); + void searchFinished(const QList &); void searchError(QString); void analyzedDuplicates(KJob *job); - void resetDates(); - /* Duplicates list related methods */ void openSelectedReport(); void itemClicked(QTreeWidgetItem *, int); @@ -91,21 +88,17 @@ void informationClicked(const QString &activatedLink); private: - bool m_searching; - bool m_foundDuplicate; + bool m_searching = false; + bool m_foundDuplicate = false; - Ui::AssistantPageBugzillaDuplicates ui; + Ui::AssistantPageBugzillaDuplicates ui; - //Dates of current Results - QDate m_startDate; - QDate m_endDate; - //Dates of searching process - QDate m_searchingStartDate; - QDate m_searchingEndDate; + KGuiItem m_searchMoreGuiItem; + KGuiItem m_retrySearchGuiItem; + DuplicateFinderJob::Result m_result; - KGuiItem m_searchMoreGuiItem; - KGuiItem m_retrySearchGuiItem; - DuplicateFinderJob::Result m_result; + int m_offset = -1; + bool m_atEnd = false; }; /** Internal bug-info dialog **/ @@ -124,7 +117,10 @@ void cancelAssistant(); private Q_SLOTS: - void bugFetchFinished(BugReport,QObject *); + void bugFetchFinished(Bugzilla::Bug::Ptr bug, QObject *); + void onCommentsFetched(QList bugComments, + QObject *jobOwner); + void bugFetchError(QString, QObject *); void reloadReport(); @@ -146,6 +142,8 @@ QString m_closedStateString; int m_duplicatesCount; QPushButton* m_suggestButton; + + Bugzilla::Bug::Ptr m_bug = nullptr; }; class BugzillaReportConfirmationDialog : public QDialog 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 @@ -19,7 +19,7 @@ #include "reportassistantpages_bugzilla_duplicates.h" -#include +#include #include #include #include @@ -36,24 +36,21 @@ //BEGIN BugzillaDuplicatesPage -BugzillaDuplicatesPage::BugzillaDuplicatesPage(ReportAssistantDialog * parent): - ReportAssistantPage(parent), - m_searching(false), - m_foundDuplicate(false) +BugzillaDuplicatesPage::BugzillaDuplicatesPage(ReportAssistantDialog *parent) + : ReportAssistantPage(parent) { - 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(); connect(ui.m_bugListWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(itemClicked(QTreeWidgetItem*,int))); - connect(ui.m_bugListWidget, &QTreeWidget::itemSelectionChanged, this, &BugzillaDuplicatesPage::itemSelectionChanged); + connect(ui.m_bugListWidget, &QTreeWidget::itemSelectionChanged, + this, &BugzillaDuplicatesPage::itemSelectionChanged); QHeaderView * header = ui.m_bugListWidget->header(); header->setSectionResizeMode(0, QHeaderView::ResizeToContents); @@ -79,8 +76,7 @@ m_searchMoreGuiItem = KGuiItem2(i18nc("@action:button", "Search for more reports"), QIcon::fromTheme(QStringLiteral("edit-find")), i18nc("@info:tooltip", "Use this button to " - "search for more similar bug reports on an " - "earlier date.")); + "search for more similar bug reports")); KGuiItem::assign(ui.m_searchMoreButton, m_searchMoreGuiItem); connect(ui.m_searchMoreButton, &QAbstractButton::clicked, this, &BugzillaDuplicatesPage::searchMore); @@ -206,38 +202,29 @@ //BEGIN Search related methods void BugzillaDuplicatesPage::searchMore() { - //1 year back - m_searchingEndDate = m_startDate; - m_searchingStartDate = m_searchingEndDate.addYears(-1); + if (m_offset < 0) { + m_offset = 0; // initialize, -1 means no search done yet + } - performSearch(); -} + // This is fairly inefficient, unfortunately the API's offset/limit system + // is not useful to us. The search is always sorting by lowest id, and + // negative offests are not a thing. So, offset=0&limit=1 gives the first + // ever reported bug in the product, while what we want is the latest. + // We also cannot query all perintent bug ids by default. While the API + // is reasonably fast, it'll still produce upwards of 2MiB just for the + // ids of a dolphin crash (as it includes all sorts of extra products). + // So we are left with somewhat shoddy time-based queries. -void BugzillaDuplicatesPage::performSearch() -{ markAsSearching(true); + ui.m_statusWidget->setBusy(i18nc("@info:status", "Searching for duplicates...")); - QString startDateStr = m_searchingStartDate.toString(QStringLiteral("yyyy-MM-dd")); - QString endDateStr = m_searchingEndDate.toString(QStringLiteral("yyyy-MM-dd")); - - ui.m_statusWidget->setBusy(i18nc("@info:status","Searching for duplicates (from %1 to %2)...", - startDateStr, endDateStr)); + // Grab the default severity for newbugs + static QString severity = reportInterface()->newBugReportTemplate().severity; - //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(); bugzillaManager()->searchBugs(reportInterface()->relatedBugzillaProducts(), - report.bugSeverity(), startDateStr, endDateStr, - reportInterface()->firstBacktraceFunctions().join(QStringLiteral(" "))); -#else //Test search - bugzillaManager()->searchBugs(QStringList() << "plasma", "crash", startDateStr, endDateStr, - "QGraphicsScenePrivate::processDirtyItemsRecursive"); -#endif + severity, + reportInterface()->firstBacktraceFunctions().join(QStringLiteral(" ")), + m_offset); } void BugzillaDuplicatesPage::stopCurrentSearch() @@ -247,12 +234,10 @@ markAsSearching(false); - if (m_startDate==m_endDate) { //Never searched - ui.m_statusWidget->setIdle(i18nc("@info:status","Search stopped.")); + if (m_offset < 0) { //Never searched + ui.m_statusWidget->setIdle(i18nc("@info:status", "Search stopped.")); } else { - ui.m_statusWidget->setIdle(i18nc("@info:status","Search stopped. Showing results from " - "%1 to %2", m_startDate.toString(QStringLiteral("yyyy-MM-dd")), - m_endDate.toString(QStringLiteral("yyyy-MM-dd")))); + ui.m_statusWidget->setIdle(i18nc("@info:status", "Search stopped. Showing results.")); } } } @@ -284,72 +269,88 @@ bool BugzillaDuplicatesPage::canSearchMore() { - return (m_startDate.year() >= 2009); + return !m_atEnd; } -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; int results = list.count(); + m_offset += results; if (results > 0) { + m_atEnd = false; + markAsSearching(false); - ui.m_statusWidget->setIdle(i18nc("@info:status","Showing results from %1 to %2", - m_startDate.toString(QStringLiteral("yyyy-MM-dd")), - m_endDate.toString(QStringLiteral("yyyy-MM-dd")))); + ui.m_statusWidget->setIdle(i18nc("@info:status", "Showing results.")); - QList bugIds; for (int i = 0; i < results; i++) { - BugMap bug = list.at(i); + Bugzilla::Bug::Ptr bug = list.at(i); - bool ok; - int bugId = bug.value(QStringLiteral("bug_id")).toInt(&ok); - if (ok) { - bugIds << bugId; - } + QString title = statusString(bug) + QLatin1Char(' ') + bug->summary(); - 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]"); - } - - title = customStatusString + QLatin1Char(' ') + bug[QStringLiteral("short_desc")]; - - 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*))); + DuplicateFinderJob *job = new DuplicateFinderJob(list, bugzillaManager(), this); + connect(job, &KJob::result, this, &BugzillaDuplicatesPage::analyzedDuplicates); job->start(); } @@ -361,6 +362,7 @@ } } else { + m_atEnd = true; if (canSearchMore()) { //We don't call markAsSearching(false) to avoid flicker @@ -380,6 +382,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 +418,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 +431,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); } @@ -440,11 +469,6 @@ "Please wait some time and try again.", err)); } -void BugzillaDuplicatesPage::resetDates() -{ - m_endDate = QDate::currentDate(); - m_startDate = m_endDate; -} //END Search related methods //BEGIN Duplicates list related methods @@ -588,9 +612,13 @@ //Connect bugzillalib signals connect(m_parent->bugzillaManager(), &BugzillaManager::bugReportFetched, - this, &BugzillaReportInformationDialog::bugFetchFinished); - connect(m_parent->bugzillaManager(), SIGNAL(bugReportError(QString,QObject*)), - this, SLOT(bugFetchError(QString,QObject*))); + this, &BugzillaReportInformationDialog::bugFetchFinished); + connect(m_parent->bugzillaManager(), &BugzillaManager::bugReportError, + this, &BugzillaReportInformationDialog::bugFetchError); + connect(m_parent->bugzillaManager(), &BugzillaManager::commentsFetched, + this, &BugzillaReportInformationDialog::onCommentsFetched); + connect(m_parent->bugzillaManager(), &BugzillaManager::commentsError, + this, &BugzillaReportInformationDialog::bugFetchError); resize(QSize(800, 600)); KConfigGroup config(KSharedConfig::openConfig(), "BugzillaReportInformationDialog"); @@ -645,173 +673,229 @@ 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 {}; +} - //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); - } +void BugzillaReportInformationDialog::bugFetchFinished(Bugzilla::Bug::Ptr bug, QObject *jobOwner) +{ + if (jobOwner != this || !isVisible()) { + return; + } - ui.m_infoBrowser->setText(text); - ui.m_infoBrowser->setEnabled(true); + if (!bug) { + 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; + } + + Q_ASSERT(!m_bug); // m_bug must only be set once we've selected one! + + // Handle duplicate state + if (bug->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)", + bug->id(), QString::number(bug->dupe_of())), + i18nc("@title:window","Nested duplicate detected"), + yesItem, + noItem); + if (ret == KMessageBox::Yes) { + qDebug() << "REDIRECT"; + showBugReport(bug->dupe_of()); + return; + } + } - m_suggestButton->setEnabled(m_relatedButtonEnabled); - m_suggestButton->setVisible(m_relatedButtonEnabled); + // Process comments... + m_bug = bug; + m_parent->bugzillaManager()->fetchComments(m_bug, this); +} - ui.m_statusWidget->setIdle(xi18nc("@info:status", "Showing bug %1", - QString::number(report.bugNumberAsInt()))); +void BugzillaReportInformationDialog::onCommentsFetched(QList bugComments, + QObject *jobOwner) +{ + if (jobOwner != this || !isVisible()) { + return; + } + + Q_ASSERT(m_bug); + + // Generate html for comments (with proper numbering) + QLatin1String duplicatesMark = QLatin1String("has been marked as a duplicate of this bug."); + + QString comments; +#warning fixme comments arent very oop + QString description; // aka first comment + if (bugComments.size() > 0) { + description = bugComments.takeFirst()->text(); + } + for (auto it = bugComments.constBegin(); it != bugComments.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 - bugComments.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(m_bug); + 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 (m_bug->severity() != QLatin1String("crash") + && m_bug->severity() != QLatin1String("major") + && m_bug->severity() != QLatin1String("grave") + && m_bug->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\"

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

Bug Report Status: %1

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

Affected Component: %1 (%2)

", + m_bug->product(), + m_bug->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(m_bug->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(); - 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()); + bug.platform = 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.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 @@ -336,13 +336,18 @@ 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() { - // NB: for practical reasons this cannot use the shared instance. Initing the instances requires - // knowing the URL already, so we'd have an init loop. Use a local static instead. + // WARNING: for practical reasons this cannot use the shared instance + // Initing the instances requires knowing the URL already, so we'd have + // an init loop. Use a local static instead. Otherwise we'd crash on + // initialization of global statics derived from our return value. + // Always copy into the local static and return that! static QString url; if (!url.isEmpty()) { return url; @@ -353,6 +358,11 @@ return url; } - url = QStringLiteral("https://bugs.kde.org/"); + if (qEnvironmentVariableIsSet("DRKONQI_TEST_MODE")) { + url = QStringLiteral("https://bugstest.kde.org/"); + } else { + url = QStringLiteral("https://bugs.kde.org/"); + } + return url; }