diff --git a/CMakeLists.txt b/CMakeLists.txt index 615bdcaa..a4e36f54 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,86 +1,90 @@ cmake_minimum_required(VERSION 3.0) project(drkonqi) set(PROJECT_VERSION "5.16.80") set(PROJECT_VERSION_MAJOR 5) set(QT_MIN_VERSION "5.12.0") set(KF5_MIN_VERSION "5.58.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH} ${ECM_KDE_MODULE_DIR}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMQtDeclareLoggingCategory) include(ECMAddTests) include(ECMMarkAsTest) 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) +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS I18n CoreAddons Service + ConfigWidgets JobWidgets KIO Crash Completion WidgetsAddons Wallet + Notifications IdleTime) if(APPLE) find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS WindowSystem) endif() find_package(Qt5X11Extras ${QT_MIN_VERSION} CONFIG) set_package_properties(Qt5X11Extras PROPERTIES TYPE RECOMMENDED PURPOSE "Recommended for better integration on X11.") if (MINGW) find_package(ZLIB REQUIRED) find_library(INTL_LIBRARY NAMES intl) find_library(IBERTY_LIBRARY NAMES iberty) find_library(BFD_LIBRARY NAMES bfd) if (IBERTY_LIBRARY) set(iberty_FOUND 1) else() set(msg "iberty") endif() if (BFD_LIBRARY) set(bfd_FOUND 1) else() set(msg "${msg} bfd") endif() if (INTL_LIBRARY) set(intl_FOUND 1) else() set(msg "${msg} intl") endif() if (msg) message(FATAL_ERROR "could not find ${msg}") endif() add_library(intl SHARED IMPORTED) set_target_properties(intl PROPERTIES IMPORTED_IMPLIB ${INTL_LIBRARY} ) add_library(iberty STATIC IMPORTED) set_target_properties(iberty PROPERTIES IMPORTED_LOCATION ${IBERTY_LIBRARY} ) add_library(bfd STATIC IMPORTED) set_target_properties(bfd PROPERTIES IMPORTED_LOCATION ${BFD_LIBRARY} # bfd header requires this to be defined INTERFACE_COMPILE_DEFINITIONS "PACKAGE;PACKAGE_VERSION" ) find_path(BFD_INCLUDE_DIR bfd.h) include_directories(${BFD_INCLUDE_DIR}) endif() include_directories("${CMAKE_CURRENT_BINARY_DIR}") add_definitions(-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT) add_subdirectory(src) if (${ECM_VERSION} STRGREATER "5.58.0") install(FILES drkonqi.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}) else() install(FILES drkonqi.categories DESTINATION ${KDE_INSTALL_CONFDIR}) endif() feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/drkonqi.categories b/drkonqi.categories index f451f436..05ab6f7c 100644 --- a/drkonqi.categories +++ b/drkonqi.categories @@ -1,2 +1,3 @@ org.kde.drkonqi drkonqi IDENTIFIER [DRKONQI_LOG] org.kde.drkonqi.parser drkonqi parser IDENTIFIER [DRKONQI_PARSER_LOG] +org.kde.drkonqi.bugzilla drkonqi bugzilla IDENTIFIER [BUGZILLA_LOG] diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5769a191..8f1766f1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,130 +1,129 @@ include (CheckFunctionExists) check_function_exists("strsignal" HAVE_STRSIGNAL) check_function_exists("uname" HAVE_UNAME) if (NOT DEBUG_PACKAGE_INSTALLER_NAME) set (DEBUG_PACKAGE_INSTALLER_NAME "installdbgsymbols.sh") endif () 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 ) find_package(KDEWin REQUIRED) # for finding drkonqi_debug.h include_directories(${CMAKE_CURRENT_BINARY_DIR}) add_subdirectory( kdbgwin ) endif () set(drkonqi_SRCS main.cpp drkonqidialog.cpp statuswidget.cpp aboutbugreportingdialog.cpp backtraceratingwidget.cpp backtracewidget.cpp backtracegenerator.cpp drkonqi.cpp drkonqibackends.cpp detachedprocessmonitor.cpp debugpackageinstaller.cpp systeminformation.cpp crashedapplication.cpp debugger.cpp debuggerlaunchers.cpp ptracer.cpp debuggermanager.cpp applicationdetailsexamples.cpp gdbhighlighter.cpp statusnotifier.cpp ) ki18n_wrap_ui(drkonqi_SRCS ui/maindialog.ui ui/backtracewidget.ui ) # if BACKTRACE_PARSER_DEBUG is enabled, it will show both the # parsed and the unparsed backtrace in the backtrace widget. # Comment this out for release. #add_definitions(-DBACKTRACE_PARSER_DEBUG) set(drkonqi_SRCS ${drkonqi_SRCS} bugzillaintegration/bugzillalib.cpp bugzillaintegration/reportassistantdialog.cpp bugzillaintegration/reportassistantpage.cpp bugzillaintegration/reportassistantpages_base.cpp bugzillaintegration/reportassistantpages_bugzilla.cpp 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 bugzillaintegration/ui/assistantpage_introduction.ui bugzillaintegration/ui/assistantpage_bugawareness.ui bugzillaintegration/ui/assistantpage_conclusions.ui bugzillaintegration/ui/assistantpage_conclusions_dialog.ui bugzillaintegration/ui/assistantpage_bugzilla_login.ui bugzillaintegration/ui/assistantpage_bugzilla_duplicates.ui bugzillaintegration/ui/assistantpage_bugzilla_duplicates_dialog.ui bugzillaintegration/ui/assistantpage_bugzilla_duplicates_dialog_confirmation.ui bugzillaintegration/ui/assistantpage_bugzilla_information.ui bugzillaintegration/ui/assistantpage_bugzilla_preview.ui bugzillaintegration/ui/assistantpage_bugzilla_send.ui ) ecm_qt_declare_logging_category(drkonqi_SRCS HEADER drkonqi_debug.h IDENTIFIER DRKONQI_LOG CATEGORY_NAME org.kde.drkonqi) - add_executable(drkonqi ${drkonqi_SRCS}) ecm_mark_nongui_executable(drkonqi) target_link_libraries(drkonqi KF5::I18n KF5::CoreAddons KF5::Service KF5::ConfigWidgets KF5::JobWidgets KF5::KIOCore KF5::Crash 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 Qt5::X11Extras ) endif() if (APPLE) target_link_libraries(drkonqi KF5::WindowSystem ) endif() if (WIN32) target_link_libraries(drkonqi kdewin) endif() install(TARGETS drkonqi DESTINATION ${KDE_INSTALL_LIBEXECDIR}) 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}) - -# 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.cpp b/src/bugzillaintegration/bugzillalib.cpp index 7d7b5765..77aebd2a 100644 --- a/src/bugzillaintegration/bugzillalib.cpp +++ b/src/bugzillaintegration/bugzillalib.cpp @@ -1,685 +1,353 @@ /******************************************************************* * 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 * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "bugzillalib.h" -#include -#include -#include -#include +#include -#include -#include -#include -#include - -#include -#include +#include "libbugzilla/clients/commentclient.h" +#include "libbugzilla/connection.h" +#include "libbugzilla/bugzilla.h" #include "drkonqi_debug.h" -#define MAKE_BUGZILLA_VERSION(a,b,c) (((a) << 16) | ((b) << 8) | (c)) +static const char showBugUrl[] = "show_bug.cgi?id=%1"; -static const char columns[] = "bug_severity,priority,bug_status,product,short_desc,resolution"; +// Extra filter rigging. We don't want to leak secrets via qdebug, so install +// a message handler which does nothing more than replace secrets in debug +// messages with placeholders. +// This is used as a global static (since message handlers are meant to be +// static) and is slightly synchronizing across threads WRT the filter hash. +struct QMessageFilterContainer { + QMessageFilterContainer(); + void insert(const QString &needle, const QString &replace); + void clear(); -//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"; + QString filter(const QString &msg); + + // Message handler is called across threads. Syncronize for good meassure. + QReadWriteLock lock; + QtMessageHandler handler; + +private: + QHash filters; +}; + +Q_GLOBAL_STATIC(QMessageFilterContainer, s_messageFilter) + +QMessageFilterContainer::QMessageFilterContainer() +{ + handler = + qInstallMessageHandler([](QtMsgType type, + const QMessageLogContext &context, + const QString &msg) { + s_messageFilter->handler(type, context, s_messageFilter->filter(msg)); +}); +} + +void QMessageFilterContainer::insert(const QString &needle, const QString &replace) +{ + if (needle.isEmpty()) { + return; + } -static inline Component buildComponent(const QVariantMap& map); -static inline Version buildVersion(const QVariantMap& map); -static inline Product buildProduct(const QVariantMap& map); + QWriteLocker locker(&lock); + filters[needle] = replace; +} -//BEGIN BugzillaManager +QString QMessageFilterContainer::filter(const QString &msg) +{ + QReadLocker locker(&lock); + QString filteredMsg = msg; + for (auto it = filters.constBegin(); it != filters.constEnd(); ++it) { + filteredMsg.replace(it.key(), it.value()); + } + return filteredMsg; +} + +void QMessageFilterContainer::clear() +{ + QWriteLocker locker(&lock); + filters.clear(); +} 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(QLatin1Char('/'))); + Bugzilla::setConnection(new Bugzilla::HTTPConnection(QUrl(m_bugTrackerUrl + QStringLiteral("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. + qCWarning(DRKONQI_LOG) << e.whatString(); + } + }); } void BugzillaManager::setFeaturesForVersion(const QString& version) { // A procedure to change Dr Konqi behaviour automatically when Bugzilla // software versions change. // // Changes should be added to Dr Konqi AHEAD of when the corresponding // Bugzilla software changes are released into bugs.kde.org, so that // Dr Konqi can continue to operate smoothly, without bug reports and a // reactive KDE software release. // // If Bugzilla announces a change to its software that affects Dr Konqi, // add executable code to implement the change automatically when the // Bugzilla software version changes. It goes at the end of this procedure // and elsewhere in this class (BugzillaManager) and/or other classes where // the change should actually be implemented. const int nVersionParts = 3; QString seps = QLatin1String("[._-]"); QStringList digits = version.split(QRegExp(seps), QString::SkipEmptyParts); while (digits.count() < nVersionParts) { digits << QLatin1String("0"); } 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); + s_messageFilter->clear(); + s_messageFilter->insert(password, QStringLiteral("PASSWORD")); + KJob *job = Bugzilla::login(username, password); + connect(job, &KJob::finished, this, [this](KJob *job) { + try { + auto details = Bugzilla::login(job); + m_token = details.token; + if (m_token.isEmpty()) { + throw Bugzilla::RuntimeException(QStringLiteral("Did not receive a token")); + } + s_messageFilter->insert(m_token, QStringLiteral("TOKEN")); + Bugzilla::connection().setToken(m_token); + m_logged = true; + emit loginFinished(true); + } catch (Bugzilla::Exception &e) { + qCWarning(DRKONQI_LOG) << e.whatString(); + // 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 { return m_logged; } QString BugzillaManager::getUsername() const { 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)); + Bugzilla::BugSearch search; + search.id = bugnumber; - if (!jobOwner) { - jobOwner = this; - } - - 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) { + qCWarning(DRKONQI_LOG) << e.whatString(); + 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.getFromBug(bug->id()); + connect(job, &KJob::finished, this, [this, &client, jobOwner](KJob *job) { + try { + auto comments = client.getFromBug(job); + emit commentsFetched(comments, jobOwner); + } catch (Bugzilla::Exception &e) { + qCWarning(DRKONQI_LOG) << e.whatString(); + 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 << QStringLiteral("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) { + qCWarning(DRKONQI_LOG) << e.whatString(); + 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) { + qCWarning(DRKONQI_LOG) << e.whatString(); + 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) { + qCWarning(DRKONQI_LOG) << e.whatString(); + 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); -} - -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); + 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) { + qCWarning(DRKONQI_LOG) << e.whatString(); + emit addMeToCCError(e.whatString()); + } + }); +} + +void BugzillaManager::fetchProductInfo(const QString &product) +{ + 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) { + qCWarning(DRKONQI_LOG) << e.whatString(); + // This doesn't have a string because it is actually not used for + // anything... + 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); } void BugzillaManager::stopCurrentSearch() { if (m_searchJob) { //Stop previous searchJob m_searchJob->disconnect(); m_searchJob->kill(); 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/bugzillalib.h b/src/bugzillaintegration/bugzillalib.h index 20b03dd8..d007f162 100644 --- a/src/bugzillaintegration/bugzillalib.h +++ b/src/bugzillaintegration/bugzillalib.h @@ -1,444 +1,105 @@ /******************************************************************* * 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 * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #ifndef BUGZILLALIB__H #define BUGZILLALIB__H #include -#include -#include -#include +#include -#include +#include "libbugzilla/clients/bugclient.h" +#include "libbugzilla/clients/productclient.h" +#include "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 { Q_OBJECT public: // Note: it expect the bugTrackerUrl parameter to contain the trailing slash. // so it should be "https://bugs.kde.org/", not "https://bugs.kde.org" 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/duplicatefinderjob.cpp b/src/bugzillaintegration/duplicatefinderjob.cpp index bba8c135..f7e2c099 100644 --- a/src/bugzillaintegration/duplicatefinderjob.cpp +++ b/src/bugzillaintegration/duplicatefinderjob.cpp @@ -1,130 +1,160 @@ /******************************************************************* * duplicatefinderjob.cpp * Copyright 2011 Matthias Fuchs +* 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 * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "duplicatefinderjob.h" #include "drkonqi_debug.h" #include "backtracegenerator.h" #include "parser/backtraceparser.h" #include "debuggermanager.h" #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() { } void DuplicateFinderJob::start() { analyzeNextBug(); } DuplicateFinderJob::Result DuplicateFinderJob::result() const { return m_result; } 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()); + } else if (unknownStatus || unknownResolution) { + // A resolution is unknown when the bug is unresolved. + // Status generally is never unknown. 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/duplicatefinderjob.h b/src/bugzillaintegration/duplicatefinderjob.h index 17ac6abe..9979d1b1 100644 --- a/src/bugzillaintegration/duplicatefinderjob.h +++ b/src/bugzillaintegration/duplicatefinderjob.h @@ -1,95 +1,93 @@ /******************************************************************* * duplicatefinderjob.h * Copyright 2011 Matthias Fuchs +* 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 * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #ifndef DUPLICATE_FINDER_H #define DUPLICATE_FINDER_H #include #include #include "bugzillalib.h" /** * Looks if of the current backtrace is a * duplicate of any of the specified bug ids. * If a duplicate is found result is emitted instantly */ class DuplicateFinderJob : public KJob { Q_OBJECT 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 * useful for example to inform the user that their * backtrace is a duplicate of this bug, which is * tracked at another number though. * * @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; /** * Call this after result has been emitted to * get the result */ 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/libbugzilla/CMakeLists.txt b/src/bugzillaintegration/libbugzilla/CMakeLists.txt new file mode 100644 index 00000000..642b2eda --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/CMakeLists.txt @@ -0,0 +1,39 @@ +set(lib_SRCS + apijob.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/bug.cpp + models/comment.cpp + models/logindetails.cpp + models/product.cpp +) + +ecm_qt_declare_logging_category(lib_SRCS + HEADER bugzilla_debug.h + IDENTIFIER BUGZILLA_LOG + CATEGORY_NAME org.kde.drkonqi.bugzilla + DEFAULT_SEVERITY Warning) + +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.cpp b/src/bugzillaintegration/libbugzilla/apijob.cpp new file mode 100644 index 00000000..76fffe4f --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/apijob.cpp @@ -0,0 +1,129 @@ +/* + 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 "bugzilla_debug.h" +#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 (const auto &a : qAsConst(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)) { + qCDebug(BUGZILLA_LOG) << "auto starting"; + start(); + } + KJob::connectNotify(signal); +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/apijob.h b/src/bugzillaintegration/libbugzilla/apijob.h new file mode 100644 index 00000000..f8e6ea54 --- /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/autotests/CMakeLists.txt b/src/bugzillaintegration/libbugzilla/autotests/CMakeLists.txt new file mode 100644 index 00000000..1305df3b --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/CMakeLists.txt @@ -0,0 +1,27 @@ +remove_definitions(-DQT_NO_CAST_FROM_ASCII) + +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 index 00000000..fc0a6e0b --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/attachmenttest.cpp @@ -0,0 +1,132 @@ +/* + 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()))); + return nullptr; + } +}; + +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 index 00000000..e0cdc64f --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/bugtest.cpp @@ -0,0 +1,239 @@ +/* + 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") }; + } + if (path == "/bug" && query.toString() == "product=dragonplayer2") { + return new JobDouble { QFINDTESTDATA("data/bugs.unresolved.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 testSearchUnresolved() + { + Bugzilla::BugSearch search; + search.products = QStringList { "dragonplayer2" }; + auto job = Bugzilla::BugClient().search(search); + job->start(); + QList bugs = Bugzilla::BugClient().search(job); + QCOMPARE(bugs.size(), 1); + // resolution:"" maps to NONE + QCOMPARE(bugs.at(0)->resolution(), Bug::Resolution::NONE); + // None of the above should fail assertions or exception tests. + } + + 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 index 00000000..0b83e574 --- /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 index 00000000..c03d5a5b --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/commenttest.cpp @@ -0,0 +1,144 @@ +/* + 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") }; + } + if (path == "/bug/1/comment" && query.toString().isEmpty()) { + return new JobDouble { QFINDTESTDATA("data/error.nobug.invalid.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"); + } + + void testSearchNoBugInvalid() + { + // Our bugzilla has a bug where errors do not have error:true! + // Make sure we correctly handle objects that are errors but do not + // necessarily have error:true. + // This is particularly relevant for comments because we make + // expectations about bugs being valid/invalid/throwing. + Bugzilla::CommentClient c; + auto job = c.getFromBug(1); + job->start(); + QVERIFY_EXCEPTION_THROWN(c.getFromBug(job), Bugzilla::APIException); + } + +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 index 00000000..bda8a01a --- /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, + socket, [&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 index 00000000..5b0c440d --- /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 index 00000000..d4146507 --- /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 index 00000000..eddb0471 --- /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.unresolved.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.unresolved.json new file mode 100644 index 00000000..5ffac975 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.unresolved.json @@ -0,0 +1,55 @@ +{ + "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" : [], + "cc_detail" : [], + "cf_commitlink" : "", + "cf_versionfixedin" : "", + "classification" : "Unclassified", + "component" : "general", + "creation_time" : "2008-06-16T21:29:47Z", + "creator" : "flabbergasted@gmx.de", + "creator_detail" : { + "email" : "flabbergasted@gmx.de", + "id" : 80677, + "name" : "flabbergasted@gmx.de", + "real_name" : "Tobias" + }, + "deadline" : null, + "depends_on" : [], + "dupe_of" : null, + "flags" : [], + "groups" : [], + "id" : 164250, + "is_cc_accessible" : null, + "is_confirmed" : null, + "is_creator_accessible" : null, + "is_open" : null, + "keywords" : [], + "last_change_time" : "2008-06-17T18:36:52Z", + "op_sys" : "Linux", + "platform" : "Ubuntu Packages", + "priority" : "NOR", + "product" : "dragonplayer", + "qa_contact" : "", + "resolution" : "", + "see_also" : [], + "severity" : "wishlist", + "status" : "UNCONFIRMED", + "summary" : "Save position of DVDs", + "target_milestone" : "---", + "url" : "", + "version" : "2.0.x", + "whiteboard" : "" + } + ] +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugs.update.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.update.json new file mode 100644 index 00000000..290d103f --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/bugs.update.json @@ -0,0 +1,10 @@ +{ + "bugs": [ + { + "id": 54321, + "changes": {}, + "alias": [], + "last_change_time": "2019-06-27T13:40:31Z" + } + ] +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.login.json b/src/bugzillaintegration/libbugzilla/autotests/data/bugzilla.login.json new file mode 100644 index 00000000..a758a370 --- /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 index 00000000..0f37533f --- /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 index 00000000..c1d5895b --- /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 index 00000000..004644c2 --- /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/error.nobug.invalid.json b/src/bugzillaintegration/libbugzilla/autotests/data/error.nobug.invalid.json new file mode 100644 index 00000000..cea36c87 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/data/error.nobug.invalid.json @@ -0,0 +1,6 @@ +{ + "code" : 101, + "documentation" : "https://bugzilla.readthedocs.org/en/5.0/api/", + "error" : null, + "message" : "Bug #1 does not exist." +} diff --git a/src/bugzillaintegration/libbugzilla/autotests/data/hi.http b/src/bugzillaintegration/libbugzilla/autotests/data/hi.http new file mode 100644 index 00000000..d638cca3 --- /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 index 00000000..58bbab84 --- /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 index 00000000..e1747a61 --- /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 index 00000000..582931ad --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/autotests/producttest.cpp @@ -0,0 +1,133 @@ +/* + 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 + +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 == "/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.cpp b/src/bugzillaintegration/libbugzilla/bugzilla.cpp new file mode 100644 index 00000000..a81fd87a --- /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/bugzilla.h b/src/bugzillaintegration/libbugzilla/bugzilla.h new file mode 100644 index 00000000..8438b32b --- /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/clients/attachmentclient.cpp b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.cpp new file mode 100644 index 00000000..c1d8cbbe --- /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/attachmentclient.h b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.h new file mode 100644 index 00000000..159ff18a --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/attachmentclient.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 ATTACHMENTCLIENT_H +#define ATTACHMENTCLIENT_H + +#include "clientbase.h" +#include "commands/newattachment.h" + +namespace Bugzilla { + +class AttachmentClient : public ClientBase +{ +public: + using ClientBase::ClientBase; + + /// Attach to a bug. @returns list of bugs that were attached to. + QList createAttachment(KJob *kjob); + KJob *createAttachment(int bugId, const NewAttachment &attachment); +}; + +} // namespace Bugzilla + +#endif // ATTACHMENTCLIENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/bugclient.cpp b/src/bugzillaintegration/libbugzilla/clients/bugclient.cpp new file mode 100644 index 00000000..eac2a766 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/bugclient.cpp @@ -0,0 +1,82 @@ +/* + 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 { + +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) +{ + APIJob *job = qobject_cast(kjob); + + qint64 ret = job->object().value(QStringLiteral("id")).toInt(-1); + Q_ASSERT(ret != -1); + return ret; +} + +KJob *BugClient::create(const NewBug &bug) +{ + return m_connection.post(QStringLiteral("/bug"), + bug.toJson()); +} + +qint64 BugClient::update(KJob *kjob) +{ + APIJob *job = qobject_cast(kjob); + + auto ary = job->object().value(QStringLiteral("bugs")).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() == 1); + + int value = ary.at(0).toObject().value(QStringLiteral("id")).toInt(-1); + Q_ASSERT(value != -1); + + return value; +} + +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/bugclient.h b/src/bugzillaintegration/libbugzilla/clients/bugclient.h new file mode 100644 index 00000000..ce0cbb16 --- /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/clientbase.cpp b/src/bugzillaintegration/libbugzilla/clients/clientbase.cpp new file mode 100644 index 00000000..00868695 --- /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/clientbase.h b/src/bugzillaintegration/libbugzilla/clients/clientbase.h new file mode 100644 index 00000000..699d6090 --- /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/commands/bugsearch.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.cpp new file mode 100644 index 00000000..d132aa10 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.cpp @@ -0,0 +1,47 @@ +/* + 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(QLatin1Char(','))); + } + seen << QStringLiteral("order"); + + expandQuery(query, seen); + + return query; +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.h b/src/bugzillaintegration/libbugzilla/clients/commands/bugsearch.h new file mode 100644 index 00000000..1d0b935a --- /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/bugupdate.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.cpp new file mode 100644 index 00000000..d049274b --- /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/bugupdate.h b/src/bugzillaintegration/libbugzilla/clients/commands/bugupdate.h new file mode 100644 index 00000000..9b8b7f44 --- /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/commandbase.h b/src/bugzillaintegration/libbugzilla/clients/commands/commandbase.h new file mode 100644 index 00000000..c5816a8b --- /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.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.cpp new file mode 100644 index 00000000..00632952 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.cpp @@ -0,0 +1,69 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#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(); + 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 == QStringLiteral("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/jsoncommand.h b/src/bugzillaintegration/libbugzilla/clients/commands/jsoncommand.h new file mode 100644 index 00000000..6c8f0864 --- /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/newattachment.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.cpp new file mode 100644 index 00000000..8a045472 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commands/newattachment.cpp @@ -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 . +*/ + +#include "newattachment.h" + +#include +#include + +namespace Bugzilla { + +QVariantHash NewAttachment::toVariantHash() const +{ + auto hash = JsonCommand::toVariantHash(); + + QVariantList idsVariant; + for (int id : ids) { + idsVariant << QVariant::fromValue(id); + } + hash[QStringLiteral("ids")] = idsVariant; + hash[QStringLiteral("data")] = data.toUtf8().toBase64(); + + 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 index 00000000..d30f3792 --- /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/newbug.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/newbug.cpp new file mode 100644 index 00000000..b456348a --- /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/newbug.h b/src/bugzillaintegration/libbugzilla/clients/commands/newbug.h new file mode 100644 index 00000000..56a8a8fb --- /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/querycommand.cpp b/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.cpp new file mode 100644 index 00000000..d6e14a56 --- /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(QStringLiteral("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/commands/querycommand.h b/src/bugzillaintegration/libbugzilla/clients/commands/querycommand.h new file mode 100644 index 00000000..33c4b7ba --- /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/commentclient.cpp b/src/bugzillaintegration/libbugzilla/clients/commentclient.cpp new file mode 100644 index 00000000..3aeb8e2b --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commentclient.cpp @@ -0,0 +1,51 @@ +/* + 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(); + + // The API should never return anything other than the single bug we asked for. + 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; +} + +KJob *CommentClient::getFromBug(int bugId) +{ + return m_connection.get(QStringLiteral("/bug/%1/comment").arg(QString::number(bugId))); +} + +} // namespace Bugzilla diff --git a/src/bugzillaintegration/libbugzilla/clients/commentclient.h b/src/bugzillaintegration/libbugzilla/clients/commentclient.h new file mode 100644 index 00000000..60802669 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/clients/commentclient.h @@ -0,0 +1,40 @@ +/* + 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 "clientbase.h" +#include "models/comment.h" + +namespace Bugzilla { + +class CommentClient : public ClientBase +{ +public: + using ClientBase::ClientBase; + + QList getFromBug(KJob *kjob); + KJob *getFromBug(int bugId); +}; + +} // namespace Bugzilla + +#endif // COMMENTCLIENT_H diff --git a/src/bugzillaintegration/libbugzilla/clients/productclient.cpp b/src/bugzillaintegration/libbugzilla/clients/productclient.cpp new file mode 100644 index 00000000..a09a186f --- /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/clients/productclient.h b/src/bugzillaintegration/libbugzilla/clients/productclient.h new file mode 100644 index 00000000..96abdcc7 --- /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/connection.cpp b/src/bugzillaintegration/libbugzilla/connection.cpp new file mode 100644 index 00000000..b8cbb4f6 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/connection.cpp @@ -0,0 +1,110 @@ +/* + Copyright 2019 Harald Sitter + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) version 3, or any + later version accepted by the membership of KDE e.V. (or its + successor approved by the membership of KDE e.V.), which shall + act as a proxy defined in Section 6 of version 3 of the license. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library. If not, see . +*/ + +#include "connection.h" + +#include +#include + +#include + +#include "bugzilla_debug.h" + +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 +{ + qCDebug(BUGZILLA_LOG) << path << query.toString(); + 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 +{ + qCDebug(BUGZILLA_LOG) << path << query.toString(); + 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 +{ + qCDebug(BUGZILLA_LOG) << path << query.toString(); + 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); + + if (!m_token.isEmpty()) { + query.addQueryItem(QStringLiteral("token"), m_token); + } + + url.setQuery(query); + return url; +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/connection.h b/src/bugzillaintegration/libbugzilla/connection.h new file mode 100644 index 00000000..69a75de6 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/connection.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 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(QStringLiteral("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/exceptions.cpp b/src/bugzillaintegration/libbugzilla/exceptions.cpp new file mode 100644 index 00000000..5e255326 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/exceptions.cpp @@ -0,0 +1,120 @@ +/* + 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(QStringLiteral("error")).toBool(m_isError); + m_message = object.value(QStringLiteral("message")).toString(m_message); + m_code = object.value(QStringLiteral("code")).toInt(m_code); + // Our bugzilla is a bit bugged. It doesn't necessarily set error to true + // but instead keeps it at null. Because of this we need to possibly shimy + // the bool to align with reality. + if (object.value(QStringLiteral("error")).type() == QJsonValue::Null && + m_code > 0 && + !m_message.isNull()) { + m_isError = true; + } +} + +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); +} + +Exception::~Exception() +{ + delete m_what; +} + +const char *Exception::what() const noexcept +{ + strcpy(m_what, qUtf8Printable(whatString())); + return m_what; +} + +RuntimeException::RuntimeException(const QString &reason) + : m_reason(reason) +{ +} + +QString RuntimeException::whatString() const +{ + return m_reason; +} + +} // namespace Bugzilla + diff --git a/src/bugzillaintegration/libbugzilla/exceptions.h b/src/bugzillaintegration/libbugzilla/exceptions.h new file mode 100644 index 00000000..78713962 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/exceptions.h @@ -0,0 +1,116 @@ +/* + 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 ~Exception(); + + virtual QString whatString() const = 0; + virtual const char *what() const noexcept override; + +private: + char *m_what = nullptr; +}; + +/** + * 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 convenience 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/models/bug.cpp b/src/bugzillaintegration/libbugzilla/models/bug.cpp new file mode 100644 index 00000000..f8d77213 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/bug.cpp @@ -0,0 +1,186 @@ +/* + 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(); + if (m_status == Status::Unknown) { + // Intentionally uncategorized. Very important warning! + qWarning() << "Drkonqi status mapping failed on bug" + << id() << ":" << status + << "Please file a bug at bugs.kde.org"; + } + + const QString resolution = obj.value(QStringLiteral("resolution")).toString(); + if (resolution.isEmpty() && m_resolution == Resolution::Unknown) { + m_resolution = Resolution::NONE; + // The empty string is unresolved. This is expected and shouldn't trip + // the mapping guard. + } else if (m_resolution == Resolution::Unknown) { + // Intentionally uncategorized. Very important warning! + qWarning() << "Drkonqi resolution mapping failed on bug" + << id() << ":" << resolution + << "Please file a bug at bugs.kde.org"; + } +} + +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/bug.h b/src/bugzillaintegration/libbugzilla/models/bug.h new file mode 100644 index 00000000..a61b3a06 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/bug.h @@ -0,0 +1,150 @@ +/* + 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. + NONE, // Fake value, expresses unresoled. On the REST side this is an empty string. + 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); + + 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 = Status::Unknown; + Resolution m_resolution = Resolution::Unknown; + qint64 m_dupe_of = -1; +}; + +} // namespace Bugzilla + +#endif // BUG_H diff --git a/src/bugzillaintegration/libbugzilla/models/comment.cpp b/src/bugzillaintegration/libbugzilla/models/comment.cpp new file mode 100644 index 00000000..04763310 --- /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/comment.h b/src/bugzillaintegration/libbugzilla/models/comment.h new file mode 100644 index 00000000..e32371aa --- /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/logindetails.cpp b/src/bugzillaintegration/libbugzilla/models/logindetails.cpp new file mode 100644 index 00000000..203bccac --- /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/logindetails.h b/src/bugzillaintegration/libbugzilla/models/logindetails.h new file mode 100644 index 00000000..9a254181 --- /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/product.cpp b/src/bugzillaintegration/libbugzilla/models/product.cpp new file mode 100644 index 00000000..31729e19 --- /dev/null +++ b/src/bugzillaintegration/libbugzilla/models/product.cpp @@ -0,0 +1,165 @@ +/* + 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) +{ + registerVariantConverters(); + + 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::registerVariantConverters() +{ + // The way List QVariant get converted to QList is a bit meh. + // A List variant by default only can convert to a QStringList, regardless + // of the T itself being a metatype known to QVariant. i.e. the QVariant + // may know how to iterate a QList, and it may know how to convert T, but + // it doesn't know how to put the two together into a list conversion. + // This is true for all Ts. You can have a QList, QVariant::fromValue + // that into a QVariant{QVariantList} but that variant will no longer + // convert>. + // To bridge the conversion gap we need to register custom converters which + // iterate the variant list and turn it into the relevant type. + + static bool convertersRegistered = false; + if (convertersRegistered) { + return; + } + convertersRegistered = true; + + QMetaType::registerConverter>( + [](QVariantList v) -> QList + { + QList list; + list.reserve(v.size()); + for (const QVariant &variant : qAsConst(v)) { + list.append(new ProductComponent(variant.toHash())); + } + return list; + }); + + QMetaType::registerConverter>( + [](QVariantList v) -> QList + { + QList list; + list.reserve(v.size()); + for (const QVariant &variant : qAsConst(v)) { + list.append(new ProductVersion(variant.toHash())); + } + return list; + }); +} + +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 diff --git a/src/bugzillaintegration/libbugzilla/models/product.h b/src/bugzillaintegration/libbugzilla/models/product.h new file mode 100644 index 00000000..076b6727 --- /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: + static void registerVariantConverters(); + + const Connection &m_connection; + + bool m_active = false; + QList m_components; + QList m_versions; +}; + +} // namespace Bugzilla + +Q_DECLARE_METATYPE(Bugzilla::ProductComponent *) +Q_DECLARE_METATYPE(QList) +Q_DECLARE_METATYPE(Bugzilla::ProductVersion *) +Q_DECLARE_METATYPE(QList) + +#endif // PRODUCT_H diff --git a/src/bugzillaintegration/parsebugbacktraces.cpp b/src/bugzillaintegration/parsebugbacktraces.cpp index c5b9a853..7bddf571 100644 --- a/src/bugzillaintegration/parsebugbacktraces.cpp +++ b/src/bugzillaintegration/parsebugbacktraces.cpp @@ -1,172 +1,169 @@ /******************************************************************* * parsebugbacktraces.cpp * Copyright 2011 Matthias Fuchs * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "parsebugbacktraces.h" #include "parser/backtraceparser.h" typedef QList::const_iterator BacktraceConstIterator; BacktraceConstIterator findCrashStackFrame(BacktraceConstIterator it, BacktraceConstIterator itEnd) { BacktraceConstIterator result = itEnd; //find the beginning of the crash for ( ; it != itEnd; ++it) { if (it->type() == BacktraceLine::KCrash) { result = it; break; } } //find the beginning of the stack frame for (it = result; it != itEnd; ++it) { if (it->type() == BacktraceLine::StackFrame) { result = it; break; } } return result; } //TODO improve this stuff, it is just a HACK ParseBugBacktraces::DuplicateRating rating(BacktraceConstIterator it, BacktraceConstIterator itEnd, BacktraceConstIterator it2, BacktraceConstIterator itEnd2) { int matches = 0; int lines = 0; it = findCrashStackFrame(it, itEnd); it2 = findCrashStackFrame(it2, itEnd2); while (it != itEnd && it2 != itEnd2) { if (it->type() == BacktraceLine::StackFrame && it2->type() == BacktraceLine::StackFrame) { ++lines; if (it->frameNumber() == it2->frameNumber() && it->functionName() == it2->functionName()) { ++matches; } ++it; ++it2; continue; } //if iters do not point to emptylines or a stackframe increase them if (it->type() != BacktraceLine::StackFrame && it->type() != BacktraceLine::EmptyLine) { ++it; continue; } if (it2->type() != BacktraceLine::StackFrame && it2->type() != BacktraceLine::EmptyLine) { ++it2; continue; } //one bt is shorter than the other if (it->type() == BacktraceLine::StackFrame && it2->type() == BacktraceLine::EmptyLine) { ++lines; ++it; continue; } if (it2->type() == BacktraceLine::StackFrame && it->type() == BacktraceLine::EmptyLine) { ++lines; ++it2; continue; } if (it->type() == BacktraceLine::EmptyLine && it2->type() == BacktraceLine::EmptyLine) { //done break; } } if (!lines) { return ParseBugBacktraces::NoDuplicate; } const int rating = matches * 100 / lines; if (rating == 100) { return ParseBugBacktraces::PerfectDuplicate; } else if (rating >= 90) { return ParseBugBacktraces::MostLikelyDuplicate; } else if (rating >= 60) { return ParseBugBacktraces::MaybeDuplicate; } else { return ParseBugBacktraces::NoDuplicate; } } -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()); } } void ParseBugBacktraces::parse(const QString &comment) { emit starting(); int start = 0; int end = -1; do { start = end + 1; end = comment.indexOf(QLatin1Char('\n'), start); emit newLine(comment.mid(start, (end != -1 ? end - start + 1 : end))); } while (end != -1); //accepts anything as backtrace, the start of the backtrace is searched later anyway m_backtraces << m_parser->parsedBacktraceLines(); } ParseBugBacktraces::DuplicateRating ParseBugBacktraces::findDuplicate(const QList &backtrace) { if (m_backtraces.isEmpty() || backtrace.isEmpty()) { return NoDuplicate; } DuplicateRating bestRating = NoDuplicate; DuplicateRating currentRating = NoDuplicate; QList >::const_iterator itBts; QList >::const_iterator itEndBts = m_backtraces.constEnd(); for (itBts = m_backtraces.constBegin(); itBts != itEndBts; ++itBts) { currentRating = rating(backtrace.constBegin(), backtrace.constEnd(), itBts->constBegin(), itBts->constEnd()); if (currentRating < bestRating) { bestRating = currentRating; } if (bestRating == PerfectDuplicate) { return bestRating; } } return bestRating; } diff --git a/src/bugzillaintegration/parsebugbacktraces.h b/src/bugzillaintegration/parsebugbacktraces.h index ca01e336..3ba76443 100644 --- a/src/bugzillaintegration/parsebugbacktraces.h +++ b/src/bugzillaintegration/parsebugbacktraces.h @@ -1,63 +1,63 @@ /******************************************************************* * parsebugbacktraces.h * Copyright 2011 Matthias Fuchs * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #ifndef PARSE_BUG_BACKTRACES_H #define PARSE_BUG_BACKTRACES_H #include "parser/backtraceline.h" #include "bugzillalib.h" class BacktraceParser; /** * Parses a Bugreport to find all the backtraces listed there * NOTE it assumes that the backtraces provided were created * by gdb */ class ParseBugBacktraces : QObject { Q_OBJECT public: - explicit ParseBugBacktraces(const BugReport &bug, QObject *parent = nullptr); + explicit ParseBugBacktraces(const QList &comments, QObject *parent = nullptr); void parse(); enum DuplicateRating { PerfectDuplicate,//functionnames and stackframe numer match MostLikelyDuplicate,//functionnames and stackframe numer match >=90% MaybeDuplicate,//functionnames and stackframe numer match >=60% NoDuplicate//functionnames and stackframe numer match <60% }; DuplicateRating findDuplicate(const QList &backtrace); Q_SIGNALS: void starting(); void newLine(const QString &line); private: void parse(const QString &comment); private: BacktraceParser *m_parser = nullptr; - const BugReport m_bug; + const QList m_comments; QList > m_backtraces; }; #endif diff --git a/src/bugzillaintegration/productmapping.cpp b/src/bugzillaintegration/productmapping.cpp index 1ce6b860..70833aa3 100644 --- a/src/bugzillaintegration/productmapping.cpp +++ b/src/bugzillaintegration/productmapping.cpp @@ -1,185 +1,185 @@ /******************************************************************* * productmapping.cpp * Copyright 2009 Dario Andres Rodriguez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "productmapping.h" #include #include #include "drkonqi_debug.h" #include #include "bugzillalib.h" #include "crashedapplication.h" ProductMapping::ProductMapping(const CrashedApplication * crashedApp, BugzillaManager * bzManager, QObject * parent) : QObject(parent) , m_crashedAppPtr(crashedApp) , m_bugzillaManagerPtr(bzManager) , m_bugzillaProductDisabled(false) , m_bugzillaVersionDisabled(false) { //Default "fallback" values m_bugzillaProduct = crashedApp->fakeExecutableBaseName(); m_bugzillaComponent = QStringLiteral("general"); m_bugzillaVersionString = QStringLiteral("unspecified"); m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct; map(crashedApp->fakeExecutableBaseName()); //Get valid versions connect(m_bugzillaManagerPtr, &BugzillaManager::productInfoFetched, this, &ProductMapping::checkProductInfo); m_bugzillaManagerPtr->fetchProductInfo(m_bugzillaProduct); } void ProductMapping::map(const QString & appName) { mapUsingInternalFile(appName); getRelatedProductsUsingInternalFile(m_bugzillaProduct); } void ProductMapping::mapUsingInternalFile(const QString & appName) { KConfig mappingsFile(QString::fromLatin1("mappings"), KConfig::NoGlobals, QStandardPaths::DataLocation); const KConfigGroup mappings = mappingsFile.group("Mappings"); if (mappings.hasKey(appName)) { QString mappingString = mappings.readEntry(appName); if (!mappingString.isEmpty()) { QStringList list = mappingString.split(QLatin1Char('|'), QString::SkipEmptyParts); if (list.count()==2) { m_bugzillaProduct = list.at(0); m_bugzillaComponent = list.at(1); m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct; } else { qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Sections found " << list.count(); } } else { qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty " "(or there was an error when reading)"; } } } void ProductMapping::getRelatedProductsUsingInternalFile(const QString & bugzillaProduct) { //ProductGroup -> kontact=kdepim //Groups -> kdepim=kontact|kmail|korganizer|akonadi|pimlibs..etc KConfig mappingsFile(QString::fromLatin1("mappings"), KConfig::NoGlobals, QStandardPaths::DataLocation); const KConfigGroup productGroup = mappingsFile.group("ProductGroup"); //Get groups of the application QStringList groups; if (productGroup.hasKey(bugzillaProduct)) { QString group = productGroup.readEntry(bugzillaProduct); if (group.isEmpty()) { qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty " "(or there was an error when reading)"; return; } groups = group.split(QLatin1Char('|'), QString::SkipEmptyParts); } //All KDE apps use the KDE Platform (basic libs) groups << QLatin1String("kdeplatform"); //Add the product itself m_relatedBugzillaProducts = QStringList() << m_bugzillaProduct; //Get related products of each related group Q_FOREACH( const QString & group, groups ) { const KConfigGroup bzGroups = mappingsFile.group("BZGroups"); if (bzGroups.hasKey(group)) { QString bzGroup = bzGroups.readEntry(group); if (!bzGroup.isEmpty()) { QStringList relatedGroups = bzGroup.split(QLatin1Char('|'), QString::SkipEmptyParts); if (relatedGroups.size()>0) { m_relatedBugzillaProducts.append(relatedGroups); } } else { qCWarning(DRKONQI_LOG) << "Error while reading mapping entry. Entry exists but it is empty " "(or there was an error when reading)"; } } } } -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"); } // 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 ! m_bugzillaVersionString = version; } else if (version.endsWith(QLatin1String(".00"))) { //check if there is a version on bugzilla with just ".0" const QString shorterVersion = version.left(version.size() - 1); if (allVersions.contains(shorterVersion)) { m_bugzillaVersionString = shorterVersion; } } // 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); } QStringList ProductMapping::relatedBugzillaProducts() const { return m_relatedBugzillaProducts; } QString ProductMapping::bugzillaProduct() const { return m_bugzillaProduct; } QString ProductMapping::bugzillaComponent() const { return m_bugzillaComponent; } QString ProductMapping::bugzillaVersion() const { return m_bugzillaVersionString; } bool ProductMapping::bugzillaProductDisabled() const { return m_bugzillaProductDisabled; } bool ProductMapping::bugzillaVersionDisabled() const { return m_bugzillaVersionDisabled; } diff --git a/src/bugzillaintegration/productmapping.h b/src/bugzillaintegration/productmapping.h index 181b3a5b..12e7f7b7 100644 --- a/src/bugzillaintegration/productmapping.h +++ b/src/bugzillaintegration/productmapping.h @@ -1,67 +1,71 @@ /******************************************************************* * productmapping.h * Copyright 2009 Dario Andres Rodriguez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #ifndef PRODUCTMAPPING__H #define PRODUCTMAPPING__H #include #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; QString bugzillaVersion() const; QStringList relatedBugzillaProducts() const; bool bugzillaProductDisabled() const; 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/reportassistantpages_base.cpp b/src/bugzillaintegration/reportassistantpages_base.cpp index 1d8d0e63..493fb4b6 100644 --- a/src/bugzillaintegration/reportassistantpages_base.cpp +++ b/src/bugzillaintegration/reportassistantpages_base.cpp @@ -1,462 +1,458 @@ /******************************************************************* * reportassistantpages_base.cpp * Copyright 2009 Dario Andres Rodriguez * Copyright 2009 A. L. Spehr * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "reportassistantpages_base.h" #include #include #include #include #include #include #include #include #include #include "drkonqi.h" #include "debuggermanager.h" #include "crashedapplication.h" #include "reportinterface.h" #include "parser/backtraceparser.h" #include "backtracegenerator.h" #include "backtracewidget.h" #include "drkonqi_globals.h" #include "applicationdetailsexamples.h" //BEGIN IntroductionPage IntroductionPage::IntroductionPage(ReportAssistantDialog * parent) : ReportAssistantPage(parent) { ui.setupUi(this); ui.m_warningIcon->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-warning")).pixmap(64,64)); } //END IntroductionPage //BEGIN CrashInformationPage CrashInformationPage::CrashInformationPage(ReportAssistantDialog * parent) : ReportAssistantPage(parent) { m_backtraceWidget = new BacktraceWidget(DrKonqi::debuggerManager()->backtraceGenerator(), this, true); connect(m_backtraceWidget, &BacktraceWidget::stateChanged, this, &CrashInformationPage::emitCompleteChanged); QVBoxLayout *layout = new QVBoxLayout(this); layout->setContentsMargins(0,0,0,0); layout->addWidget(m_backtraceWidget); layout->addSpacing(10); //We need this for better usability until we get something better //If the backtrace was already fetched on the main dialog, save it. BacktraceGenerator *btGenerator = DrKonqi::debuggerManager()->backtraceGenerator(); if (btGenerator->state() == BacktraceGenerator::Loaded) { BacktraceParser::Usefulness use = btGenerator->parser()->backtraceUsefulness(); if (use != BacktraceParser::Useless && use != BacktraceParser::InvalidUsefulness) { reportInterface()->setBacktrace(btGenerator->backtrace()); } } } void CrashInformationPage::aboutToShow() { m_backtraceWidget->generateBacktrace(); m_backtraceWidget->hilightExtraDetailsLabel(false); emitCompleteChanged(); } void CrashInformationPage::aboutToHide() { BacktraceGenerator *btGenerator = DrKonqi::debuggerManager()->backtraceGenerator(); BacktraceParser::Usefulness use = btGenerator->parser()->backtraceUsefulness(); if (use != BacktraceParser::Useless && use != BacktraceParser::InvalidUsefulness) { reportInterface()->setBacktrace(btGenerator->backtrace()); } reportInterface()->setFirstBacktraceFunctions(btGenerator->parser()->firstValidFunctions()); } bool CrashInformationPage::isComplete() { BacktraceGenerator *generator = DrKonqi::debuggerManager()->backtraceGenerator(); return (generator->state() != BacktraceGenerator::NotLoaded && generator->state() != BacktraceGenerator::Loading); } bool CrashInformationPage::showNextPage() { BacktraceParser::Usefulness use = DrKonqi::debuggerManager()->backtraceGenerator()->parser()->backtraceUsefulness(); if (DrKonqi::ignoreQuality()) { return true; } if ((use == BacktraceParser::InvalidUsefulness || use == BacktraceParser::ProbablyUseless || use == BacktraceParser::Useless) && m_backtraceWidget->canInstallDebugPackages()) { if ( KMessageBox::Yes == KMessageBox::questionYesNo(this, i18nc("@info","This crash information is not useful enough, " "do you want to try to improve it? You will need " "to install some debugging packages."), i18nc("@title:window","Crash Information is not useful enough")) ) { m_backtraceWidget->hilightExtraDetailsLabel(true); m_backtraceWidget->focusImproveBacktraceButton(); return false; //Cancel show next, to allow the user to write more } else { return true; //Allow to continue } } else { return true; } } //END CrashInformationPage //BEGIN BugAwarenessPage +static QHash s_reproducibleIndex { + { 0, ReportInterface::ReproducibleUnsure }, + { 1, ReportInterface::ReproducibleNever }, + { 2, ReportInterface::ReproducibleSometimes }, + { 3, ReportInterface::ReproducibleEverytime } +}; + BugAwarenessPage::BugAwarenessPage(ReportAssistantDialog * parent) : ReportAssistantPage(parent) { ui.setupUi(this); ui.m_actionsInsideApp->setText(i18nc("@option:check kind of information the user can provide " "about the crash, %1 is the application name", "What I was doing when the application \"%1\" crashed", DrKonqi::crashedApplication()->name())); connect(ui.m_rememberGroup, static_cast(&QButtonGroup::buttonClicked), this, &BugAwarenessPage::updateCheckBoxes); // Also listen to toggle so radio buttons are covered. connect(ui.m_rememberGroup, static_cast(&QButtonGroup::buttonToggled), this, &BugAwarenessPage::updateCheckBoxes); ui.m_appSpecificDetailsExamples->setVisible(reportInterface()->appDetailsExamples()->hasExamples()); ui.m_appSpecificDetailsExamples->setText( i18nc("@label examples about information the user can provide", "Examples: %1", reportInterface()->appDetailsExamples()->examples())); ui.m_appSpecificDetailsExamples->setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); + + if (qEnvironmentVariableIsSet("DRKONQI_TEST_MODE")) { + ui.m_rememberCrashSituationYes->setChecked(true); + ui.m_reproducibleBox->setCurrentIndex( + s_reproducibleIndex.key(ReportInterface::ReproducibleEverytime)); + } } void BugAwarenessPage::aboutToShow() { updateCheckBoxes(); } void BugAwarenessPage::aboutToHide() { //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, ui.m_actionsInsideApp->isChecked(), ui.m_unusualSituation->isChecked(), ui.m_appSpecificDetails->isChecked()); } void BugAwarenessPage::updateCheckBoxes() { const bool rememberSituation = ui.m_rememberCrashSituationYes->isChecked(); ui.m_reproducibleLabel->setEnabled(rememberSituation); ui.m_reproducibleBox->setEnabled(rememberSituation); ui.m_informationLabel->setEnabled(rememberSituation); ui.m_actionsInsideApp->setEnabled(rememberSituation); ui.m_unusualSituation->setEnabled(rememberSituation); ui.m_appSpecificDetails->setEnabled(rememberSituation); ui.m_appSpecificDetailsExamples->setEnabled(rememberSituation); } //END BugAwarenessPage //BEGIN ConclusionPage ConclusionPage::ConclusionPage(ReportAssistantDialog * parent) : ReportAssistantPage(parent) , m_needToReport(false) { m_isBKO = DrKonqi::crashedApplication()->bugReportAddress().isKdeBugzilla(); ui.setupUi(this); KGuiItem::assign(ui.m_showReportInformationButton, KGuiItem2(i18nc("@action:button", "&Show Contents of the Report"), QIcon::fromTheme(QStringLiteral("document-preview")), i18nc("@info:tooltip", "Use this button to show the generated " "report information about this crash."))); connect(ui.m_showReportInformationButton, &QPushButton::clicked, this, &ConclusionPage::openReportInformation); ui.m_restartAppOnFinish->setVisible(false); } void ConclusionPage::finishClicked() { //Manual report if (m_needToReport && !m_isBKO) { const CrashedApplication *crashedApp = DrKonqi::crashedApplication(); BugReportAddress reportAddress = crashedApp->bugReportAddress(); QString report = reportInterface()->generateReportFullText(false); if (reportAddress.isEmail()) { QString subject = QStringLiteral("[%1] [%2] Automatic crash report generated by DrKonqi"); subject= subject.arg(crashedApp->name()); subject= subject.arg(crashedApp->datetime().toString(QStringLiteral("yyyy-MM-dd"))); KToolInvocation::invokeMailer(reportAddress, QLatin1String(""), QLatin1String("") , subject, report); } else { QUrl url(reportAddress); if (QUrl(reportAddress).isRelative()) { //Scheme is missing url = QUrl(QString::fromLatin1("https://%1").arg(reportAddress)); } QDesktopServices::openUrl(url); } //Show a copy of the bug reported openReportInformation(); } //Restart application if (ui.m_restartAppOnFinish->isChecked()) { DrKonqi::crashedApplication()->restart(); } } void ConclusionPage::aboutToShow() { connect(assistant()->finishButton(), &QPushButton::clicked, this, &ConclusionPage::finishClicked); ui.m_restartAppOnFinish->setVisible(false); ui.m_restartAppOnFinish->setChecked(false); const bool isDuplicate = reportInterface()->duplicateId() && !reportInterface()->attachToBugNumber(); m_needToReport = reportInterface()->isWorthReporting() && !isDuplicate; emitCompleteChanged(); BugReportAddress reportAddress = DrKonqi::crashedApplication()->bugReportAddress(); BacktraceParser::Usefulness use = DrKonqi::debuggerManager()->backtraceGenerator()->parser()->backtraceUsefulness(); QString explanationHTML = QLatin1String("

    "); bool backtraceGenerated = true; switch (use) { case BacktraceParser::ReallyUseful: { explanationHTML += QStringLiteral("
  • %1
  • ").arg(i18nc("@info","The automatically generated " "crash information is useful.")); break; } case BacktraceParser::MayBeUseful: { explanationHTML += QStringLiteral("
  • %1
  • ").arg(i18nc("@info","The automatically generated " "crash information lacks some " "details " "but may be still be useful.")); break; } case BacktraceParser::ProbablyUseless: { explanationHTML += QStringLiteral("
  • %1
  • ").arg(i18nc("@info","The automatically generated " "crash information lacks important details " "and it is probably not helpful.")); break; } case BacktraceParser::Useless: case BacktraceParser::InvalidUsefulness: { BacktraceGenerator::State state = DrKonqi::debuggerManager()->backtraceGenerator()->state(); if (state == BacktraceGenerator::NotLoaded) { backtraceGenerated = false; explanationHTML += QStringLiteral("
  • %1
  • ").arg(i18nc("@info","The crash information was " "not generated because it was not needed.")); } else { explanationHTML += QStringLiteral("
  • %1
    %2
  • ").arg( i18nc("@info","The automatically generated crash " "information does not contain enough information to be " "helpful."), xi18nc("@info","You can improve it by " "installing debugging packages and reloading the crash on " "the Crash Information page. You can get help with the Bug " "Reporting Guide by clicking on the " "Help button.")); //but this guide doesn't mention bt packages? that's techbase //->>and the help guide mention techbase page... } break; } } //User can provide enough information if (reportInterface()->isBugAwarenessPageDataUseful()) { explanationHTML += QStringLiteral("
  • %1
  • ").arg(i18nc("@info","The information you can " "provide could be considered helpful.")); } else { explanationHTML += QStringLiteral("
  • %1
  • ").arg(i18nc("@info","The information you can " "provide is not considered helpful enough in this case.")); } if (isDuplicate) { explanationHTML += QStringLiteral("
  • %1
  • ").arg(xi18nc("@info","Your problem has already been " "reported as bug %1.", QString::number(reportInterface()->duplicateId()))); } explanationHTML += QLatin1String("

"); ui.m_explanationLabel->setText(explanationHTML); //Hide the "Show contents of the report" button if the backtrace was not generated ui.m_showReportInformationButton->setVisible(backtraceGenerated); if (m_needToReport) { ui.m_conclusionsLabel->setText(QStringLiteral("

%1").arg(i18nc("@info","This " "report is considered helpful."))); if (m_isBKO) { emitCompleteChanged(); ui.m_howToProceedLabel->setText(xi18nc("@info","This application's bugs are reported " "to the KDE bug tracking system: click Next" " to start the reporting process. " "You can manually report at %1", reportAddress)); } else { if (!DrKonqi::crashedApplication()->hasBeenRestarted()) { ui.m_restartAppOnFinish->setVisible(true); } ui.m_howToProceedLabel->setText(xi18nc("@info","This application is not supported in the " "KDE bug tracking system. Click " "Finish to report this bug to " "the application maintainer. Also, you can manually " "report at %1.", reportAddress)); emit finished(false); } } else { // (m_needToReport) if (!DrKonqi::crashedApplication()->hasBeenRestarted()) { ui.m_restartAppOnFinish->setVisible(true); } ui.m_conclusionsLabel->setText(QStringLiteral("

%1
%2

").arg( i18nc("@info","This report does not contain enough information for the " "developers, so the automated bug reporting process is not " "enabled for this crash."), i18nc("@info","If you wish, you can go back and change your " "answers. "))); //Only mention "manual reporting" if the backtrace was generated. //FIXME separate the texts "manual reporting" / "click finish to close" //"manual reporting" should be ~"manual report using the contents of the report".... //FIXME for 4.5 (workflow, see ToDo) if (backtraceGenerated) { if (m_isBKO) { ui.m_howToProceedLabel->setText(xi18nc("@info","You can manually report this bug " "at %1. " "Click Finish to close the " "assistant.", reportAddress)); } else { ui.m_howToProceedLabel->setText(xi18nc("@info","You can manually report this " "bug to its maintainer at %1. " "Click Finish to close the " "assistant.", reportAddress)); } } emit finished(true); } } void ConclusionPage::aboutToHide() { disconnect(assistant()->finishButton(), &QPushButton::clicked, this, &ConclusionPage::finishClicked); } void ConclusionPage::openReportInformation() { if (!m_infoDialog) { QString info = reportInterface()->generateReportFullText(false) + QLatin1Char('\n') + i18nc("@info report to url/mail address","Report to %1", DrKonqi::crashedApplication()->bugReportAddress()); m_infoDialog = new ReportInformationDialog(info); } m_infoDialog->show(); m_infoDialog->raise(); m_infoDialog->activateWindow(); } bool ConclusionPage::isComplete() { return (m_isBKO && m_needToReport); } //END ConclusionPage //BEGIN ReportInformationDialog ReportInformationDialog::ReportInformationDialog(const QString & reportText) : QDialog() { setAttribute(Qt::WA_DeleteOnClose, true); setWindowTitle(i18nc("@title:window","Contents of the Report")); ui.setupUi(this); ui.m_reportInformationBrowser->setPlainText(reportText); QPushButton* saveButton = new QPushButton(ui.buttonBox); KGuiItem::assign(saveButton, KGuiItem2(i18nc("@action:button", "&Save to File..."), QIcon::fromTheme(QStringLiteral("document-save")), i18nc("@info:tooltip", "Use this button to save the " "generated crash report information to " "a file. You can use this option to report the " "bug later."))); connect(saveButton, &QPushButton::clicked, this, &ReportInformationDialog::saveReport); ui.buttonBox->addButton(saveButton, QDialogButtonBox::ActionRole); KConfigGroup config(KSharedConfig::openConfig(), "ReportInformationDialog"); KWindowConfig::restoreWindowSize(windowHandle(), config); } ReportInformationDialog::~ReportInformationDialog() { KConfigGroup config(KSharedConfig::openConfig(), "ReportInformationDialog"); KWindowConfig::saveWindowSize(windowHandle(), config); } void ReportInformationDialog::saveReport() { DrKonqi::saveReport(ui.m_reportInformationBrowser->toPlainText(), this); } //END ReportInformationDialog diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla.cpp b/src/bugzillaintegration/reportassistantpages_bugzilla.cpp index de0ea5ed..3a70b1de 100644 --- a/src/bugzillaintegration/reportassistantpages_bugzilla.cpp +++ b/src/bugzillaintegration/reportassistantpages_bugzilla.cpp @@ -1,883 +1,813 @@ /******************************************************************* * reportassistantpages_bugzilla.cpp * Copyright 2009, 2010, 2011 Dario Andres Rodriguez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "reportassistantpages_bugzilla.h" #include #include #include #include #include #include #include #include #include #include #include #include "drkonqi_debug.h" #include #include #include #include #include /* Unhandled error dialog includes */ #include #include #include #include #include "reportinterface.h" #include "systeminformation.h" #include "crashedapplication.h" #include "bugzillalib.h" #include "statuswidget.h" #include "drkonqi.h" #include "drkonqi_globals.h" #include "applicationdetailsexamples.h" static const char kWalletEntryName[] = "drkonqi_bugzilla"; static const char kWalletEntryUsername[] = "username"; static const char kWalletEntryPassword[] = "password"; static QString konquerorKWalletEntryName = KDE_BUGZILLA_URL + QStringLiteral("index.cgi#login"); static const char konquerorKWalletEntryUsername[] = "Bugzilla_login"; static const char konquerorKWalletEntryPassword[] = "Bugzilla_password"; //BEGIN BugzillaLoginPage BugzillaLoginPage::BugzillaLoginPage(ReportAssistantDialog * parent) : ReportAssistantPage(parent), m_wallet(nullptr), m_walletWasOpenedBefore(false), m_bugzillaVersionFound(false) { connect(bugzillaManager(), &BugzillaManager::bugzillaVersionFound, this, &BugzillaLoginPage::bugzillaVersionFound); connect(bugzillaManager(), &BugzillaManager::loginFinished, this, &BugzillaLoginPage::loginFinished); connect(bugzillaManager(), &BugzillaManager::loginError, this, &BugzillaLoginPage::loginError); ui.setupUi(this); ui.m_statusWidget->setIdle(i18nc("@info:status '1' is replaced with the short URL of the bugzilla ", "You need to login with your %1 account in order to proceed.", QLatin1String(KDE_BUGZILLA_SHORT_URL))); KGuiItem::assign(ui.m_loginButton, KGuiItem2(i18nc("@action:button", "Login"), QIcon::fromTheme(QStringLiteral("network-connect")), i18nc("@info:tooltip", "Use this button to login " "to the KDE bug tracking system using the provided " "username and password."))); ui.m_loginButton->setEnabled(false); connect(ui.m_loginButton, &QPushButton::clicked, this, &BugzillaLoginPage::loginClicked); connect(ui.m_userEdit, &KLineEdit::returnPressed, this, &BugzillaLoginPage::loginClicked); connect(ui.m_passwordEdit->lineEdit(), &QLineEdit::returnPressed, this, &BugzillaLoginPage::loginClicked); connect(ui.m_userEdit, &KLineEdit::textChanged, this, &BugzillaLoginPage::updateLoginButtonStatus); connect(ui.m_passwordEdit, &KPasswordLineEdit::passwordChanged, this, &BugzillaLoginPage::updateLoginButtonStatus); ui.m_noticeLabel->setText( xi18nc("@info/rich","You need a user account on the " "KDE bug tracking system in order to " "file a bug report, because we may need to contact you later " "for requesting further information. If you do not have " "one, you can freely create one here. " "Please do not use disposable email accounts.", DrKonqi::crashedApplication()->bugReportAddress(), KDE_BUGZILLA_CREATE_ACCOUNT_URL)); } bool BugzillaLoginPage::isComplete() { return bugzillaManager()->getLogged(); } void BugzillaLoginPage::bugzillaVersionFound() { // Login depends on first knowing the Bugzilla software version number. m_bugzillaVersionFound = true; updateLoginButtonStatus(); } void BugzillaLoginPage::updateLoginButtonStatus() { ui.m_loginButton->setEnabled( !ui.m_userEdit->text().isEmpty() && !ui.m_passwordEdit->password().isEmpty() && m_bugzillaVersionFound ); } void BugzillaLoginPage::loginError(const QString & err, const QString & extendedMessage) { 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); } } void BugzillaLoginPage::aboutToShow() { if (bugzillaManager()->getLogged()) { ui.m_loginButton->setEnabled(false); ui.m_userEdit->setEnabled(false); ui.m_userEdit->clear(); ui.m_passwordEdit->setEnabled(false); ui.m_passwordEdit->clear(); ui.m_loginButton->setVisible(false); ui.m_userEdit->setVisible(false); ui.m_passwordEdit->setVisible(false); ui.m_userLabel->setVisible(false); ui.m_passwordLabel->setVisible(false); ui.m_savePasswordCheckBox->setVisible(false); ui.m_noticeLabel->setVisible(false); ui.m_statusWidget->setIdle(i18nc("@info:status the user is logged at the bugtracker site " "as USERNAME", "Logged in at the KDE bug tracking system (%1) as: %2.", QLatin1String(KDE_BUGZILLA_SHORT_URL), bugzillaManager()->getUsername())); } else { //Try to show wallet dialog once this dialog is shown QTimer::singleShot(100, this, &BugzillaLoginPage::walletLogin); } } bool BugzillaLoginPage::kWalletEntryExists(const QString& entryName) { return !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::NetworkWallet(), KWallet::Wallet::FormDataFolder(), entryName); } void BugzillaLoginPage::openWallet() { //Store if the wallet was previously opened so we can know if we should close it later m_walletWasOpenedBefore = KWallet::Wallet::isOpen(KWallet::Wallet::NetworkWallet()); //Request open the wallet m_wallet = KWallet::Wallet::openWallet(KWallet::Wallet::NetworkWallet(), static_cast(this->parent())->winId()); } void BugzillaLoginPage::walletLogin() { if (!m_wallet) { if (kWalletEntryExists(QLatin1String(kWalletEntryName))) { //Key exists! openWallet(); ui.m_savePasswordCheckBox->setCheckState(Qt::Checked); //Was the wallet opened? if (m_wallet) { m_wallet->setFolder(KWallet::Wallet::FormDataFolder()); //Use wallet data to try login QMap values; m_wallet->readMap(QLatin1String(kWalletEntryName), values); QString username = values.value(QLatin1String(kWalletEntryUsername)); QString password = values.value(QLatin1String(kWalletEntryPassword)); if (!username.isEmpty() && !password.isEmpty()) { ui.m_userEdit->setText(username); ui.m_passwordEdit->setPassword(password); } } } else if (kWalletEntryExists(konquerorKWalletEntryName)) { //If the DrKonqi entry is empty, but a Konqueror entry exists, use and copy it. openWallet(); if (m_wallet) { m_wallet->setFolder(KWallet::Wallet::FormDataFolder()); //Fetch Konqueror data QMap values; m_wallet->readMap(konquerorKWalletEntryName, values); QString username = values.value(QLatin1String(konquerorKWalletEntryUsername)); QString password = values.value(QLatin1String(konquerorKWalletEntryPassword)); if (!username.isEmpty() && !password.isEmpty()) { //Copy to DrKonqi own entries values.clear(); values.insert(QLatin1String(kWalletEntryUsername), username); values.insert(QLatin1String(kWalletEntryPassword), password); m_wallet->writeMap(QLatin1String(kWalletEntryName), values); ui.m_savePasswordCheckBox->setCheckState(Qt::Checked); ui.m_userEdit->setText(username); ui.m_passwordEdit->setPassword(password); } } } } } -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 if (!m_wallet) { openWallet(); } //Got wallet open ? if (m_wallet) { m_wallet->setFolder(KWallet::Wallet::FormDataFolder()); QMap values; values.insert(QLatin1String(kWalletEntryUsername), ui.m_userEdit->text()); values.insert(QLatin1String(kWalletEntryPassword), ui.m_passwordEdit->password()); m_wallet->writeMap(QLatin1String(kWalletEntryName), values); } } else { //User doesn't want to save or wants to remove. if (kWalletEntryExists(QLatin1String(kWalletEntryName))) { if (!m_wallet) { openWallet(); } //Got wallet open ? if (m_wallet) { m_wallet->setFolder(KWallet::Wallet::FormDataFolder()); m_wallet->removeEntry(QLatin1String(kWalletEntryName)); } } } ui.m_statusWidget->setBusy(i18nc("@info:status '1' is a url, '2' the username", "Performing login at %1 as %2...", QLatin1String(KDE_BUGZILLA_SHORT_URL), ui.m_userEdit->text())); bugzillaManager()->tryLogin(ui.m_userEdit->text(), ui.m_passwordEdit->password()); } else { loginFinished(false); } } void BugzillaLoginPage::updateWidget(bool enabled) { ui.m_loginButton->setEnabled(enabled); ui.m_userLabel->setEnabled(enabled); ui.m_passwordLabel->setEnabled(enabled); ui.m_userEdit->setEnabled(enabled); ui.m_passwordEdit->setEnabled(enabled); ui.m_savePasswordCheckBox->setEnabled(enabled); } void BugzillaLoginPage::loginFinished(bool logged) { if (logged) { emitCompleteChanged(); aboutToShow(); if (m_wallet) { if (m_wallet->isOpen() && !m_walletWasOpenedBefore) { m_wallet->lockWallet(); } } emit loggedTurnToNextPage(); } else { ui.m_statusWidget->setIdle(i18nc("@info:status","Error: Invalid username or " "password")); updateWidget(true); ui.m_userEdit->setFocus(Qt::OtherFocusReason); } } BugzillaLoginPage::~BugzillaLoginPage() { //Close wallet if we close the assistant in this step if (m_wallet) { if (m_wallet->isOpen() && !m_walletWasOpenedBefore) { m_wallet->lockWallet(); } delete m_wallet; } } //END BugzillaLoginPage //BEGIN BugzillaInformationPage BugzillaInformationPage::BugzillaInformationPage(ReportAssistantDialog * parent) : ReportAssistantPage(parent), m_textsOK(false), m_distributionComboSetup(false), m_distroComboVisible(false), m_requiredCharacters(1) { ui.setupUi(this); m_textCompleteBar = new KCapacityBar(KCapacityBar::DrawTextInline, this); ui.horizontalLayout_2->addWidget(m_textCompleteBar); connect(ui.m_titleEdit, &KLineEdit::textChanged, this, &BugzillaInformationPage::checkTexts); connect(ui.m_detailsEdit, &QTextEdit::textChanged, this, &BugzillaInformationPage::checkTexts); connect(ui.m_titleLabel, &QLabel::linkActivated, this, &BugzillaInformationPage::showTitleExamples); connect(ui.m_detailsLabel, &QLabel::linkActivated, this, &BugzillaInformationPage::showDescriptionHelpExamples); ui.m_compiledSourcesCheckBox->setChecked( DrKonqi::systemInformation()->compiledSources()); } void BugzillaInformationPage::aboutToShow() { if (!m_distributionComboSetup) { //Autodetecting distro failed ? if (DrKonqi::systemInformation()->bugzillaPlatform() == QLatin1String("unspecified")) { m_distroComboVisible = true; ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Unspecified"),QStringLiteral("unspecified")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Archlinux"), QStringLiteral("Archlinux Packages")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Chakra"), QStringLiteral("Chakra")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Debian stable"), QStringLiteral("Debian stable")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Debian testing"), QStringLiteral("Debian testing")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Debian unstable"), QStringLiteral("Debian unstable")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Exherbo"), QStringLiteral("Exherbo Packages")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Fedora"), QStringLiteral("Fedora RPMs")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Gentoo"), QStringLiteral("Gentoo Packages")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Mageia"), QStringLiteral("Mageia RPMs")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Mandriva"), QStringLiteral("Mandriva RPMs")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Neon"), QStringLiteral("Neon Packages")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "OpenSUSE"), QStringLiteral("openSUSE RPMs")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Pardus"), QStringLiteral("Pardus Packages")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "RedHat"), QStringLiteral("RedHat RPMs")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Slackware"), QStringLiteral("Slackware Packages")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Ubuntu (and derivatives)"), QStringLiteral("Ubuntu Packages")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "FreeBSD (Ports)"), QStringLiteral("FreeBSD Ports")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "NetBSD (pkgsrc)"), QStringLiteral("NetBSD pkgsrc")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "OpenBSD"), QStringLiteral("OpenBSD Packages")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Mac OS X"), QStringLiteral("MacPorts Packages")); ui.m_distroChooserCombo->addItem(i18nc("@label:listbox KDE distribution method", "Solaris"), QStringLiteral("Solaris Packages")); //Restore previously selected bugzilla platform (distribution) KConfigGroup config(KSharedConfig::openConfig(), "BugzillaInformationPage"); QString entry = config.readEntry("BugzillaPlatform","unspecified"); int index = ui.m_distroChooserCombo->findData(entry); if ( index == -1 ) index = 0; ui.m_distroChooserCombo->setCurrentIndex(index); } else { ui.m_distroChooserCombo->setVisible(false); } m_distributionComboSetup = true; } //Calculate the minimum number of characters required for a description //If creating a new report: minimum 40, maximum 80 //If attaching to an existent report: minimum 30, maximum 50 int multiplier = (reportInterface()->attachToBugNumber() == 0) ? 10 : 5; m_requiredCharacters = 20 + (reportInterface()->selectedOptionsRating() * multiplier); //Fill the description textedit with some headings: QString descriptionTemplate; if (ui.m_detailsEdit->toPlainText().isEmpty()) { if (reportInterface()->userCanProvideActionsAppDesktop()) { descriptionTemplate += QLatin1String("- What I was doing when the application crashed:\n\n"); } if (reportInterface()->userCanProvideUnusualBehavior()) { descriptionTemplate += QLatin1String("- Unusual behavior I noticed:\n\n"); } if (reportInterface()->userCanProvideApplicationConfigDetails()) { descriptionTemplate += QLatin1String("- Custom settings of the application:\n\n"); } ui.m_detailsEdit->setText(descriptionTemplate); } checkTexts(); //May be the options (canDetail) changed and we need to recheck } int BugzillaInformationPage::currentDescriptionCharactersCount() { QString description = ui.m_detailsEdit->toPlainText(); //Do not count template messages, and other misc chars description.remove(QStringLiteral("What I was doing when the application crashed")); description.remove(QStringLiteral("Unusual behavior I noticed")); description.remove(QStringLiteral("Custom settings of the application")); description.remove(QLatin1Char('\n')); description.remove(QLatin1Char('-')); description.remove(QLatin1Char(':')); description.remove(QLatin1Char(' ')); return description.size(); } void BugzillaInformationPage::checkTexts() { //If attaching this report to an existing one then the title is not needed bool showTitle = (reportInterface()->attachToBugNumber() == 0); ui.m_titleEdit->setVisible(showTitle); ui.m_titleLabel->setVisible(showTitle); bool ok = !((ui.m_titleEdit->isVisible() && ui.m_titleEdit->text().isEmpty()) || ui.m_detailsEdit->toPlainText().isEmpty()); QString message; int percent = currentDescriptionCharactersCount() * 100 / m_requiredCharacters; if (percent >= 100) { percent = 100; message = i18nc("the minimum required length of a text was reached", "Minimum length reached"); } else { message = i18nc("the minimum required length of a text wasn't reached yet", "Provide more information"); } m_textCompleteBar->setValue(percent); m_textCompleteBar->setText(message); if (ok != m_textsOK) { m_textsOK = ok; emitCompleteChanged(); } } bool BugzillaInformationPage::showNextPage() { checkTexts(); if (m_textsOK) { bool detailsShort = currentDescriptionCharactersCount() < m_requiredCharacters; if (detailsShort) { //The user input is less than we want.... encourage to write more QString message = i18nc("@info","The description about the crash details does not provide " "enough information yet.

"); message += QLatin1Char(' ') + i18nc("@info","The amount of required information is proportional to " "the quality of the other information like the backtrace " "or the reproducibility rate." "

"); if (reportInterface()->userCanProvideActionsAppDesktop() || reportInterface()->userCanProvideUnusualBehavior() || reportInterface()->userCanProvideApplicationConfigDetails()) { message += QLatin1Char(' ') + i18nc("@info","Previously, you told DrKonqi that you could provide some " "contextual information. Try writing more details about your situation. " "(even little ones could help us.)

"); } message += QLatin1Char(' ') + i18nc("@info","If you cannot provide more information, your report " "will probably waste developers' time. Can you tell us more?"); KGuiItem yesItem = KStandardGuiItem::yes(); yesItem.setText(i18n("Yes, let me add more information")); KGuiItem noItem = KStandardGuiItem::no(); noItem.setText(i18n("No, I cannot add any other information")); if (KMessageBox::warningYesNo(this, message, i18nc("@title:window","We need more information"), yesItem, noItem) == KMessageBox::No) { //Request the assistant to close itself (it will prompt for confirmation anyways) assistant()->close(); return false; } } else { return true; } } return false; } bool BugzillaInformationPage::isComplete() { return m_textsOK; } void BugzillaInformationPage::aboutToHide() { //Save fields data reportInterface()->setTitle(ui.m_titleEdit->text()); reportInterface()->setDetailText(ui.m_detailsEdit->toPlainText()); if (m_distroComboVisible) { //Save bugzilla platform (distribution) QString bugzillaPlatform = ui.m_distroChooserCombo->itemData( ui.m_distroChooserCombo->currentIndex()).toString(); KConfigGroup config(KSharedConfig::openConfig(), "BugzillaInformationPage"); config.writeEntry("BugzillaPlatform", bugzillaPlatform); DrKonqi::systemInformation()->setBugzillaPlatform(bugzillaPlatform); } bool compiledFromSources = ui.m_compiledSourcesCheckBox->checkState() == Qt::Checked; DrKonqi::systemInformation()->setCompiledSources(compiledFromSources); } void BugzillaInformationPage::showTitleExamples() { QString titleExamples = xi18nc("@info:tooltip examples of good bug report titles", "Examples of good titles:\"Plasma crashed after adding the Notes " "widget and writing on it\"\"Konqueror crashed when accessing the Facebook " "application 'X'\"\"Kopete suddenly closed after resuming the computer and " "talking to a MSN buddy\"\"Kate closed while editing a log file and pressing the " "Delete key a couple of times\""); QToolTip::showText(QCursor::pos(), titleExamples); } void BugzillaInformationPage::showDescriptionHelpExamples() { QString descriptionHelp = i18nc("@info:tooltip help and examples of good bug descriptions", "Describe in as much detail as possible the crash circumstances:"); if (reportInterface()->userCanProvideActionsAppDesktop()) { descriptionHelp += QStringLiteral("
") + i18nc("@info:tooltip help and examples of good bug descriptions", "- Detail which actions were you taking inside and outside the " "application an instant before the crash."); } if (reportInterface()->userCanProvideUnusualBehavior()) { descriptionHelp += QStringLiteral("
") + i18nc("@info:tooltip help and examples of good bug descriptions", "- Note if you noticed any unusual behavior in the application " "or in the whole environment."); } if (reportInterface()->userCanProvideApplicationConfigDetails()) { descriptionHelp += QStringLiteral("
") + i18nc("@info:tooltip help and examples of good bug descriptions", "- Note any non-default configuration in the application."); if (reportInterface()->appDetailsExamples()->hasExamples()) { descriptionHelp += QLatin1Char(' ') + i18nc("@info:tooltip examples of configuration details. " "the examples are already translated", "Examples: %1", reportInterface()->appDetailsExamples()->examples()); } } QToolTip::showText(QCursor::pos(), descriptionHelp); } //END BugzillaInformationPage //BEGIN BugzillaPreviewPage BugzillaPreviewPage::BugzillaPreviewPage(ReportAssistantDialog * parent) : ReportAssistantPage(parent) { ui.setupUi(this); } void BugzillaPreviewPage::aboutToShow() { ui.m_previewEdit->setText(reportInterface()->generateReportFullText(true)); } //END BugzillaPreviewPage //BEGIN BugzillaSendPage BugzillaSendPage::BugzillaSendPage(ReportAssistantDialog * parent) : ReportAssistantPage(parent), m_contentsDialog(nullptr) { connect(reportInterface(), &ReportInterface::reportSent, this, &BugzillaSendPage::sent); connect(reportInterface(), &ReportInterface::sendReportError, this, &BugzillaSendPage::sendError); ui.setupUi(this); KGuiItem::assign(ui.m_retryButton, KGuiItem2(i18nc("@action:button", "Retry..."), QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@info:tooltip", "Use this button to retry " "sending the crash report if it failed before."))); KGuiItem::assign(ui.m_showReportContentsButton, KGuiItem2(i18nc("@action:button", "Sho&w Contents of the Report"), QIcon::fromTheme(QStringLiteral("document-preview")), i18nc("@info:tooltip", "Use this button to show the generated " "report information about this crash."))); connect(ui.m_showReportContentsButton, &QPushButton::clicked, this, &BugzillaSendPage::openReportContents); ui.m_retryButton->setVisible(false); connect(ui.m_retryButton, &QAbstractButton::clicked, this , &BugzillaSendPage::retryClicked); ui.m_launchPageOnFinish->setVisible(false); ui.m_restartAppOnFinish->setVisible(false); connect(assistant()->finishButton(), &QPushButton::clicked, this, &BugzillaSendPage::finishClicked); } void BugzillaSendPage::retryClicked() { ui.m_retryButton->setEnabled(false); aboutToShow(); } void BugzillaSendPage::aboutToShow() { ui.m_statusWidget->setBusy(i18nc("@info:status","Sending crash report... (please wait)")); reportInterface()->sendBugReport(); } void BugzillaSendPage::sent(int bug_id) { ui.m_statusWidget->setVisible(false); ui.m_retryButton->setEnabled(false); ui.m_retryButton->setVisible(false); ui.m_showReportContentsButton->setVisible(false); ui.m_launchPageOnFinish->setVisible(true); ui.m_restartAppOnFinish->setVisible(!DrKonqi::crashedApplication()->hasBeenRestarted()); ui.m_restartAppOnFinish->setChecked(false); reportUrl = bugzillaManager()->urlForBug(bug_id); ui.m_finishedLabel->setText(xi18nc("@info/rich","Crash report sent." "URL: %1" "Thank you for being part of KDE. " "You can now close this window.", reportUrl)); emit finished(false); } void BugzillaSendPage::sendError(const QString & errorString, const QString & extendedMessage) { ui.m_statusWidget->setIdle(xi18nc("@info:status","Error sending the crash report: " "%1.", errorString)); ui.m_retryButton->setEnabled(true); ui.m_retryButton->setVisible(true); if (!extendedMessage.isEmpty()) { new UnhandledErrorDialog(this,errorString, extendedMessage); } } void BugzillaSendPage::finishClicked() { if (ui.m_launchPageOnFinish->isChecked() && !reportUrl.isEmpty()) { QDesktopServices::openUrl(QUrl(reportUrl)); } if (ui.m_restartAppOnFinish->isChecked()) { DrKonqi::crashedApplication()->restart(); } } void BugzillaSendPage::openReportContents() { if (!m_contentsDialog) { QString report = reportInterface()->generateReportFullText(false) + QLatin1Char('\n') + i18nc("@info report to KDE bugtracker address","Report to %1", DrKonqi::crashedApplication()->bugReportAddress()); m_contentsDialog = new ReportInformationDialog(report); } m_contentsDialog->show(); m_contentsDialog->raise(); m_contentsDialog->activateWindow(); } //END BugzillaSendPage /* Dialog for Unhandled Bugzilla Errors */ /* The user can save the bugzilla html output to check the error and/or to report this as a DrKonqi bug */ //BEGIN UnhandledErrorDialog UnhandledErrorDialog::UnhandledErrorDialog(QWidget * parent, const QString & error, const QString & extendedMessage) : QDialog(parent) { setWindowTitle(i18nc("@title:window", "Unhandled Bugzilla Error")); setWindowModality(Qt::ApplicationModal); QPushButton* saveButton = new QPushButton(this); saveButton->setText(i18nc("@action:button save html to a file","Save to a file")); saveButton->setIcon(QIcon::fromTheme(QStringLiteral("document-save"))); connect(saveButton, &QPushButton::clicked, this, &UnhandledErrorDialog::saveErrorMessage); setAttribute(Qt::WA_DeleteOnClose); QTextBrowser * htmlView = new QTextBrowser(this); QLabel * iconLabel = new QLabel(this); iconLabel->setFixedSize(32, 32); iconLabel->setPixmap(QIcon::fromTheme(QStringLiteral("dialog-warning")).pixmap(32, 32)); QLabel * mainLabel = new QLabel(this); mainLabel->setWordWrap(true); mainLabel->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); QHBoxLayout * titleLayout = new QHBoxLayout(); titleLayout->setContentsMargins(5,2,5,2); titleLayout->setSpacing(5); titleLayout->addWidget(iconLabel); titleLayout->addWidget(mainLabel); QDialogButtonBox* buttonBox = new QDialogButtonBox(this); buttonBox->setStandardButtons(QDialogButtonBox::Close); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); QVBoxLayout * layout = new QVBoxLayout(this); layout->addLayout(titleLayout); layout->addWidget(htmlView); layout->addWidget(buttonBox); m_extendedHTMLError = extendedMessage; mainLabel->setText(i18nc("@label", "There was an unhandled Bugzilla error: %1.
" "Below is the HTML that DrKonqi received. " "Try to perform the action again or save this error page " "to submit a bug against DrKonqi.").arg(error)); htmlView->setHtml(extendedMessage); show(); } void UnhandledErrorDialog::saveErrorMessage() { QString defaultName = QLatin1String("drkonqi-unhandled-bugzilla-error.html"); QPointer dlg(new QFileDialog(this)); dlg->selectFile(defaultName); dlg->setWindowTitle(i18nc("@title:window","Select Filename")); dlg->setAcceptMode(QFileDialog::AcceptSave); dlg->setFileMode(QFileDialog::AnyFile); dlg->setConfirmOverwrite(true); if ( dlg->exec() == QDialog::Accepted ) { if (!dlg) { //Dialog closed externally (ex. via DBus) return; } QUrl fileUrl; if(!dlg->selectedUrls().isEmpty()) fileUrl = dlg->selectedUrls().first(); if (fileUrl.isValid()) { QTemporaryFile tf; if (tf.open()) { QTextStream ts(&tf); ts << m_extendedHTMLError; ts.flush(); } else { KMessageBox::sorry(this, xi18nc("@info","Cannot open file %1 " "for writing.", tf.fileName())); delete dlg; return; } KIO::FileCopyJob* job = KIO::file_copy(QUrl::fromLocalFile(tf.fileName()), fileUrl); KJobWidgets::setWindow(job, this); if (!job->exec()) { KMessageBox::sorry(this, job->errorString()); } } } delete dlg; } //END UnhandledErrorDialog diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla.h b/src/bugzillaintegration/reportassistantpages_bugzilla.h index 86b642da..40d6bfcf 100644 --- a/src/bugzillaintegration/reportassistantpages_bugzilla.h +++ b/src/bugzillaintegration/reportassistantpages_bugzilla.h @@ -1,164 +1,163 @@ /******************************************************************* * reportassistantpages_bugzilla.h * Copyright 2009, 2011 Dario Andres Rodriguez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #ifndef REPORTASSISTANTPAGES__BUGZILLA__H #define REPORTASSISTANTPAGES__BUGZILLA__H #include "reportassistantpage.h" #include "reportassistantpages_base.h" #include "ui_assistantpage_bugzilla_login.h" #include "ui_assistantpage_bugzilla_information.h" #include "ui_assistantpage_bugzilla_preview.h" #include "ui_assistantpage_bugzilla_send.h" namespace KWallet { class Wallet; } class KCapacityBar; /** Bugzilla login **/ class BugzillaLoginPage: public ReportAssistantPage { Q_OBJECT public: explicit BugzillaLoginPage(ReportAssistantDialog *); ~BugzillaLoginPage() override; void aboutToShow() override; bool isComplete() override; private Q_SLOTS: void bugzillaVersionFound(); void loginClicked(); void loginFinished(bool); void loginError(const QString &, const QString &); void walletLogin(); void updateLoginButtonStatus(); Q_SIGNALS: void loggedTurnToNextPage(); private: void updateWidget(bool enabled); bool kWalletEntryExists(const QString&); void openWallet(); - bool canSetCookies(); Ui::AssistantPageBugzillaLogin ui; KWallet::Wallet * m_wallet; bool m_walletWasOpenedBefore; bool m_bugzillaVersionFound; }; /** Title and details page **/ class BugzillaInformationPage : public ReportAssistantPage { Q_OBJECT public: explicit BugzillaInformationPage(ReportAssistantDialog *); void aboutToShow() override; void aboutToHide() override; bool isComplete() override; bool showNextPage() override; private Q_SLOTS: void showTitleExamples(); void showDescriptionHelpExamples(); void checkTexts(); private: int currentDescriptionCharactersCount(); Ui::AssistantPageBugzillaInformation ui; KCapacityBar * m_textCompleteBar; bool m_textsOK; bool m_distributionComboSetup; bool m_distroComboVisible; int m_requiredCharacters; }; /** Preview report page **/ class BugzillaPreviewPage : public ReportAssistantPage { Q_OBJECT public: explicit BugzillaPreviewPage(ReportAssistantDialog *); void aboutToShow() override; private: Ui::AssistantPageBugzillaPreview ui; }; /** Send crash report page **/ class BugzillaSendPage : public ReportAssistantPage { Q_OBJECT public: explicit BugzillaSendPage(ReportAssistantDialog *); void aboutToShow() override; private Q_SLOTS: void sent(int); void sendError(const QString &, const QString &); void retryClicked(); void finishClicked(); void openReportContents(); private: Ui::AssistantPageBugzillaSend ui; QString reportUrl; QPointer m_contentsDialog; Q_SIGNALS: void finished(bool); }; class UnhandledErrorDialog: public QDialog { Q_OBJECT public: UnhandledErrorDialog(QWidget * parent, const QString &, const QString &); private Q_SLOTS: void saveErrorMessage(); private: QString m_extendedHTMLError; }; #endif diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp index 98ea9147..c91920f3 100644 --- a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp +++ b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.cpp @@ -1,992 +1,1070 @@ /******************************************************************* * reportassistantpages_bugzilla_duplicates.cpp * Copyright 2009 Dario Andres Rodriguez +* 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 * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "reportassistantpages_bugzilla_duplicates.h" -#include +#include #include #include #include #include #include #include #include #include #include "drkonqi_globals.h" #include "reportinterface.h" #include "statuswidget.h" //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); header->setSectionResizeMode(1, QHeaderView::Interactive); //Create manual bug report entry (first one) QTreeWidgetItem * customBugItem = new QTreeWidgetItem( QStringList() << i18nc("@item:intable custom/manaul bug report number", "Manual") << i18nc("@item:intable custom bug report number description", "Manually enter a bug report ID")); customBugItem->setData(0, Qt::UserRole, QLatin1String("custom")); customBugItem->setIcon(1, QIcon::fromTheme(QStringLiteral("edit-rename"))); QString helpMessage = i18nc("@info:tooltip / whatsthis", "Select this option to manually load a specific bug report"); customBugItem->setToolTip(0, helpMessage); customBugItem->setToolTip(1, helpMessage); customBugItem->setWhatsThis(0, helpMessage); customBugItem->setWhatsThis(1, helpMessage); ui.m_bugListWidget->addTopLevelItem(customBugItem); 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); m_retrySearchGuiItem = KGuiItem2(i18nc("@action:button", "Retry search"), QIcon::fromTheme(QStringLiteral("edit-find")), i18nc("@info:tooltip", "Use this button to " "retry the search that previously " "failed.")); KGuiItem::assign(ui.m_openReportButton, KGuiItem2(i18nc("@action:button", "Open selected report"), QIcon::fromTheme(QStringLiteral("document-preview")), i18nc("@info:tooltip", "Use this button to view " "the information of the selected bug report."))); connect(ui.m_openReportButton, &QAbstractButton::clicked, this, &BugzillaDuplicatesPage::openSelectedReport); KGuiItem::assign(ui.m_stopSearchButton, KGuiItem2(i18nc("@action:button", "Stop searching"), QIcon::fromTheme(QStringLiteral("process-stop")), i18nc("@info:tooltip", "Use this button to stop " "the current search."))); ui.m_stopSearchButton->setText(QString()); //FIXME connect(ui.m_stopSearchButton, &QAbstractButton::clicked, this, &BugzillaDuplicatesPage::stopCurrentSearch); //Possible duplicates list and buttons connect(ui.m_selectedDuplicatesList, SIGNAL(itemDoubleClicked(QListWidgetItem*)), this, SLOT(itemClicked(QListWidgetItem*))); connect(ui.m_selectedDuplicatesList, &QListWidget::itemSelectionChanged, this, &BugzillaDuplicatesPage::possibleDuplicateSelectionChanged); KGuiItem::assign(ui.m_removeSelectedDuplicateButton, KGuiItem2(i18nc("@action:button remove the selected item from a list", "Remove"), QIcon::fromTheme(QStringLiteral("list-remove")), i18nc("@info:tooltip", "Use this button to remove a selected possible duplicate"))); ui.m_removeSelectedDuplicateButton->setEnabled(false); connect(ui.m_removeSelectedDuplicateButton, &QAbstractButton::clicked, this, &BugzillaDuplicatesPage::removeSelectedDuplicate); ui.m_attachToReportIcon->setPixmap(QIcon::fromTheme(QStringLiteral("mail-attachment")).pixmap(16,16)); ui.m_attachToReportIcon->setFixedSize(16,16); ui.m_attachToReportIcon->setVisible(false); ui.m_attachToReportLabel->setVisible(false); ui.m_attachToReportLabel->setContextMenuPolicy(Qt::NoContextMenu); connect(ui.m_attachToReportLabel, &QLabel::linkActivated, this, &BugzillaDuplicatesPage::cancelAttachToBugReport); ui.information->setContextMenuPolicy(Qt::NoContextMenu); connect(ui.information, &QLabel::linkActivated, this, &BugzillaDuplicatesPage::informationClicked); showDuplicatesPanel(false); } BugzillaDuplicatesPage::~BugzillaDuplicatesPage() { } void BugzillaDuplicatesPage::aboutToShow() { //Perform initial search if we are not currently searching and if there are no results yet if (!m_searching && ui.m_bugListWidget->topLevelItemCount() == 1 && canSearchMore()) { searchMore(); } } void BugzillaDuplicatesPage::aboutToHide() { stopCurrentSearch(); //Save selected possible duplicates by user QStringList possibleDuplicates; int count = ui.m_selectedDuplicatesList->count(); for(int i = 0; iitem(i)->text(); } reportInterface()->setPossibleDuplicates(possibleDuplicates); //Save possible duplicates by query QStringList duplicatesByQuery; count = ui.m_bugListWidget->topLevelItemCount(); for(int i = 1; itopLevelItem(i)->text(0); } reportInterface()->setPossibleDuplicatesByQuery(duplicatesByQuery); } bool BugzillaDuplicatesPage::isComplete() { return !m_searching; } bool BugzillaDuplicatesPage::showNextPage() { //Ask the user to check all the possible duplicates... if (ui.m_bugListWidget->topLevelItemCount() != 1 && ui.m_selectedDuplicatesList->count() == 0 && reportInterface()->attachToBugNumber() == 0 && !m_foundDuplicate) { //The user didn't selected any possible duplicate nor a report to attach the new info. //Double check this, we need to reduce the duplicate count. KGuiItem noDuplicatesButton; noDuplicatesButton.setText(i18n("There are no real duplicates")); noDuplicatesButton.setWhatsThis(i18n("Press this button to declare that, in your opinion " "and according to your experience, the reports found " "as similar do not match the crash you have " "experienced, and you believe it is unlikely that a " "better match would be found after further review.")); noDuplicatesButton.setIcon(QIcon::fromTheme(QStringLiteral("dialog-cancel"))); KGuiItem letMeCheckMoreReportsButton; letMeCheckMoreReportsButton.setText(i18n("Let me check more reports")); letMeCheckMoreReportsButton.setWhatsThis(i18n("Press this button if you would rather " "review more reports in order to find a " "match for the crash you have experienced.")); letMeCheckMoreReportsButton.setIcon(QIcon::fromTheme(QStringLiteral("document-preview"))); if (KMessageBox::questionYesNo(this, i18nc("@info","You have not selected any possible duplicates, or a report to which to attach your " "crash information. Have you read all the reports, and can you confirm that there are no " "real duplicates?"), i18nc("@title:window","No selected possible duplicates"), letMeCheckMoreReportsButton, noDuplicatesButton) == KMessageBox::Yes) { return false; } } return true; } //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")); + // Grab the default severity for newbugs + static QString severity = reportInterface()->newBugReportTemplate().severity; - ui.m_statusWidget->setBusy(i18nc("@info:status","Searching for duplicates (from %1 to %2)...", - startDateStr, endDateStr)); - - //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() { if (m_searching) { bugzillaManager()->stopCurrentSearch(); 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.")); } } } void BugzillaDuplicatesPage::markAsSearching(bool searching) { m_searching = searching; emitCompleteChanged(); ui.m_bugListWidget->setEnabled(!searching); ui.m_searchMoreButton->setEnabled(!searching); ui.m_searchMoreButton->setVisible(!searching); ui.m_stopSearchButton->setEnabled(searching); ui.m_stopSearchButton->setVisible(searching); ui.m_selectedDuplicatesList->setEnabled(!searching); ui.m_selectedPossibleDuplicatesLabel->setEnabled(!searching); ui.m_removeSelectedDuplicateButton->setEnabled(!searching && !ui.m_selectedDuplicatesList->selectedItems().isEmpty()); ui.m_attachToReportLabel->setEnabled(!searching); if (!searching) { itemSelectionChanged(); } else { ui.m_openReportButton->setEnabled(false); } } bool BugzillaDuplicatesPage::canSearchMore() { - return (m_startDate.year() >= 2009); + return !m_atEnd; +} + +static QString statusString(const Bugzilla::Bug::Ptr &bug) +{ + // 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: + case Bugzilla::Bug::Resolution::NONE: + return QStringLiteral("[%1]").arg(QVariant::fromValue(bug->resolution()).toString()); + case Bugzilla::Bug::Resolution::Unknown: + Q_FALLTHROUGH(); + } + break; + case Bugzilla::Bug::Status::NEEDSINFO: + return i18nc("@info bug status", "[Incomplete]"); + + case Bugzilla::Bug::Status::Unknown: + Q_FALLTHROUGH(); + } + return QString(); } -void BugzillaDuplicatesPage::searchFinished(const BugMapList & list) +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); - - bool ok; - int bugId = bug.value(QStringLiteral("bug_id")).toInt(&ok); - if (ok) { - bugIds << bugId; - } + Bugzilla::Bug::Ptr bug = list.at(i); - 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")]; + QString title = statusString(bug) + QLatin1Char(' ') + bug->summary(); - QStringList fields = QStringList() << bug[QStringLiteral("bug_id")] << title; + QStringList fields = QStringList() << QString::number(bug->id()) << title; QTreeWidgetItem * item = new QTreeWidgetItem(fields); - item->setToolTip(0, bug[QStringLiteral("short_desc")]); - item->setToolTip(1, bug[QStringLiteral("short_desc")]); + item->setToolTip(0, bug->summary()); + item->setToolTip(1, bug->summary()); ui.m_bugListWidget->addTopLevelItem(item); } if (!m_foundDuplicate) { markAsSearching(true); - DuplicateFinderJob *job = new DuplicateFinderJob(bugIds, bugzillaManager(), this); - connect(job, SIGNAL(result(KJob*)), this, SLOT(analyzedDuplicates(KJob*))); + DuplicateFinderJob *job = new DuplicateFinderJob(list, bugzillaManager(), this); + connect(job, &KJob::result, this, &BugzillaDuplicatesPage::analyzedDuplicates); job->start(); } ui.m_bugListWidget->sortItems(0 , Qt::DescendingOrder); ui.m_bugListWidget->resizeColumnToContents(1); if (!canSearchMore()) { ui.m_searchMoreButton->setEnabled(false); } } else { + m_atEnd = true; if (canSearchMore()) { //We don't call markAsSearching(false) to avoid flicker //Delayed call to searchMore to avoid unexpected behaviour (signal/slot) //because we are in a slot, and searchMore() will be ending calling this slot again QTimer::singleShot(0, this, &BugzillaDuplicatesPage::searchMore); } else { markAsSearching(false); ui.m_statusWidget->setIdle(i18nc("@info:status","Search Finished. " "No reports found.")); ui.m_searchMoreButton->setEnabled(false); if (ui.m_bugListWidget->topLevelItemCount() == 0) { //No reports to mark as possible duplicate ui.m_selectedDuplicatesList->setEnabled(false); } } } } +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_FALLTHROUGH(); + } + return false; +} + +static bool isStatusClosed(Bugzilla::Bug::Status status) +{ + return !isStatusOpen(status); +} + void BugzillaDuplicatesPage::analyzedDuplicates(KJob *j) { markAsSearching(false); DuplicateFinderJob *job = static_cast(j); m_result = job->result(); m_foundDuplicate = m_result.parentDuplicate; 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; if (m_foundDuplicate) { const QList items = ui.m_bugListWidget->findItems(QString::number(parentDuplicate), Qt::MatchExactly, 0); const QBrush brush = KColorScheme(QPalette::Active, KColorScheme::View).background(KColorScheme::NeutralBackground); Q_FOREACH (QTreeWidgetItem* item, items) { for (int i = 0; i < item->columnCount(); ++i) { item->setBackground(i, brush); } } 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); } } void BugzillaDuplicatesPage::informationClicked(const QString &activatedLink) { if (activatedLink == QLatin1String("attach")) { attachToBugReport(m_result.parentDuplicate); } else { int number = activatedLink.toInt(); if (number) { showReportInformationDialog(number, false); } } } void BugzillaDuplicatesPage::searchError(QString err) { KGuiItem::assign(ui.m_searchMoreButton, m_retrySearchGuiItem); markAsSearching(false); ui.m_statusWidget->setIdle(i18nc("@info:status","Error fetching the bug report list")); KMessageBox::error(this , xi18nc("@info/rich","Error fetching the bug report list" "%1." "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 void BugzillaDuplicatesPage::openSelectedReport() { QList selected = ui.m_bugListWidget->selectedItems(); if (selected.count() == 1) { itemClicked(selected.at(0), 0); } } void BugzillaDuplicatesPage::itemClicked(QTreeWidgetItem * item, int col) { Q_UNUSED(col); int bugNumber = 0; if (item->data(0, Qt::UserRole) == QLatin1String("custom")) { bool ok = false; bugNumber = QInputDialog::getInt(this, i18nc("@title:window", "Enter a custom bug report number"), i18nc("@label", "Enter the number of the bug report you want to check"), 0, 0, 1000000, 1, &ok); } else { bugNumber = item->text(0).toInt(); } showReportInformationDialog(bugNumber); } void BugzillaDuplicatesPage::itemClicked(QListWidgetItem * item) { showReportInformationDialog(item->text().toInt()); } void BugzillaDuplicatesPage::showReportInformationDialog(int bugNumber, bool relatedButtonEnabled) { if (bugNumber <= 0) { return; } BugzillaReportInformationDialog * infoDialog = new BugzillaReportInformationDialog(this); connect(infoDialog, &BugzillaReportInformationDialog::possibleDuplicateSelected, this, &BugzillaDuplicatesPage::addPossibleDuplicateNumber); connect(infoDialog, &BugzillaReportInformationDialog::attachToBugReportSelected, this, &BugzillaDuplicatesPage::attachToBugReport); infoDialog->showBugReport(bugNumber, relatedButtonEnabled); } void BugzillaDuplicatesPage::itemSelectionChanged() { ui.m_openReportButton->setEnabled(ui.m_bugListWidget->selectedItems().count() == 1); } //END Duplicates list related methods //BEGIN Selected duplicates list related methods void BugzillaDuplicatesPage::addPossibleDuplicateNumber(int bugNumber) { QString stringNumber = QString::number(bugNumber); if (ui.m_selectedDuplicatesList->findItems(stringNumber, Qt::MatchExactly).isEmpty()) { ui.m_selectedDuplicatesList->addItem(stringNumber); } showDuplicatesPanel(true); } void BugzillaDuplicatesPage::removeSelectedDuplicate() { QList items = ui.m_selectedDuplicatesList->selectedItems(); if (items.length() > 0) { delete ui.m_selectedDuplicatesList->takeItem(ui.m_selectedDuplicatesList->row(items.at(0))); } if (ui.m_selectedDuplicatesList->count() == 0) { showDuplicatesPanel(false); } } void BugzillaDuplicatesPage::showDuplicatesPanel(bool show) { ui.m_removeSelectedDuplicateButton->setVisible(show); ui.m_selectedDuplicatesList->setVisible(show); ui.m_selectedPossibleDuplicatesLabel->setVisible(show); } void BugzillaDuplicatesPage::possibleDuplicateSelectionChanged() { ui.m_removeSelectedDuplicateButton->setEnabled( !ui.m_selectedDuplicatesList->selectedItems().isEmpty()); } //END Selected duplicates list related methods //BEGIN Attach to bug related methods void BugzillaDuplicatesPage::attachToBugReport(int bugNumber) { ui.m_attachToReportLabel->setText(xi18nc("@label", "The report is going to be " "attached to bug %1. " "Cancel", QString::number(bugNumber))); ui.m_attachToReportLabel->setVisible(true); ui.m_attachToReportIcon->setVisible(true); reportInterface()->setAttachToBugNumber(bugNumber); } void BugzillaDuplicatesPage::cancelAttachToBugReport() { ui.m_attachToReportLabel->setVisible(false); ui.m_attachToReportIcon->setVisible(false); reportInterface()->setAttachToBugNumber(0); } //END Attach to bug related methods //END BugzillaDuplicatesPage //BEGIN BugzillaReportInformationDialog BugzillaReportInformationDialog::BugzillaReportInformationDialog(BugzillaDuplicatesPage * parent) : QDialog(parent), m_relatedButtonEnabled(true), m_parent(parent), m_bugNumber(0), m_duplicatesCount(0) { setWindowTitle(i18nc("@title:window","Bug Description")); ui.setupUi(this); KGuiItem::assign(ui.m_retryButton, KGuiItem2(i18nc("@action:button", "Retry..."), QIcon::fromTheme(QStringLiteral("view-refresh")), i18nc("@info:tooltip", "Use this button to retry " "loading the bug report."))); connect(ui.m_retryButton, &QPushButton::clicked, this, &BugzillaReportInformationDialog::reloadReport); m_suggestButton = new QPushButton(this); ui.buttonBox->addButton(m_suggestButton, QDialogButtonBox::ActionRole); KGuiItem::assign(m_suggestButton, KGuiItem2(i18nc("@action:button", "Suggest this crash is related"), QIcon::fromTheme(QStringLiteral("list-add")), i18nc("@info:tooltip", "Use this button to suggest that " "the crash you experienced is related to this bug " "report"))); connect(m_suggestButton, &QPushButton::clicked, this, &BugzillaReportInformationDialog::relatedReportClicked); connect(ui.m_showOwnBacktraceCheckBox, &QAbstractButton::toggled, this, &BugzillaReportInformationDialog::toggleShowOwnBacktrace); //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); KConfigGroup config(KSharedConfig::openConfig(), "BugzillaReportInformationDialog"); KWindowConfig::restoreWindowSize(windowHandle(), config); } BugzillaReportInformationDialog::~BugzillaReportInformationDialog() { KConfigGroup config(KSharedConfig::openConfig(), "BugzillaReportInformationDialog"); KWindowConfig::saveWindowSize(windowHandle(), config); } void BugzillaReportInformationDialog::reloadReport() { showBugReport(m_bugNumber); } void BugzillaReportInformationDialog::showBugReport(int bugNumber, bool relatedButtonEnabled) { m_relatedButtonEnabled = relatedButtonEnabled; ui.m_retryButton->setVisible(false); m_closedStateString.clear(); m_bugNumber = bugNumber; m_parent->bugzillaManager()->fetchBugReport(m_bugNumber, this); m_suggestButton->setEnabled(false); m_suggestButton->setVisible(m_relatedButtonEnabled); ui.m_infoBrowser->setText(i18nc("@info:status","Loading...")); ui.m_infoBrowser->setEnabled(false); ui.m_linkLabel->setText(xi18nc("@info","Report's webpage", m_parent->bugzillaManager()->urlForBug(m_bugNumber))); ui.m_statusWidget->setBusy(xi18nc("@info:status","Loading information about bug " "%1 from %2....", QString::number(m_bugNumber), QLatin1String(KDE_BUGZILLA_SHORT_URL))); ui.m_backtraceBrowser->setPlainText( i18nc("@info","Backtrace of the crash I experienced:\n\n") + m_parent->reportInterface()->backtrace()); KConfigGroup config(KSharedConfig::openConfig(), "BugzillaReportInformationDialog"); bool showOwnBacktrace = config.readEntry("ShowOwnBacktrace", false); ui.m_showOwnBacktraceCheckBox->setChecked(showOwnBacktrace); if (!showOwnBacktrace) { //setChecked(false) will not emit toggled(false) toggleShowOwnBacktrace(false); } 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: + case Bugzilla::Bug::Resolution::NONE: + return { QVariant::fromValue(bug->resolution()).toString(), QString() }; + case Bugzilla::Bug::Resolution::Unknown: + Q_FALLTHROUGH(); + } + 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_FALLTHROUGH(); + } + return {}; +} - //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.

"); - } +void BugzillaReportInformationDialog::bugFetchFinished(Bugzilla::Bug::Ptr bug, QObject *jobOwner) +{ + if (jobOwner != this || !isVisible()) { + 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); - } + 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; + } + } + + // Process comments... + m_bug = bug; + m_parent->bugzillaManager()->fetchComments(m_bug, this); +} - ui.m_infoBrowser->setText(text); - ui.m_infoBrowser->setEnabled(true); +void BugzillaReportInformationDialog::onCommentsFetched(QList bugComments, + QObject *jobOwner) +{ + if (jobOwner != this || !isVisible()) { + return; + } + + Q_ASSERT(m_bug); - m_suggestButton->setEnabled(m_relatedButtonEnabled); - m_suggestButton->setVisible(m_relatedButtonEnabled); + // Generate html for comments (with proper numbering) + QLatin1String duplicatesMark = QLatin1String("has been marked as a duplicate of this bug."); - ui.m_statusWidget->setIdle(xi18nc("@info:status", "Showing bug %1", - QString::number(report.bugNumberAsInt()))); + // TODO: the way comment objects are turned into comment strings is fairly + // awkward and does not particularly object-centric. May benefit from a + // slight redesign. + QString comments; + 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() { emit possibleDuplicateSelected(m_bugNumber); hide(); } void BugzillaReportInformationDialog::attachToBugReport() { emit attachToBugReportSelected(m_bugNumber); hide(); } void BugzillaReportInformationDialog::cancelAssistant() { m_parent->assistant()->close(); hide(); } void BugzillaReportInformationDialog::relatedReportClicked() { BugzillaReportConfirmationDialog * confirmation = new BugzillaReportConfirmationDialog(m_bugNumber, (m_duplicatesCount >= 10), m_closedStateString, this); confirmation->show(); } void BugzillaReportInformationDialog::bugFetchError(QString err, QObject * jobOwner) { if (jobOwner == this && isVisible()) { KMessageBox::error(this , xi18nc("@info/rich","Error fetching the bug report" "%1." "Please wait some time and try again.", err)); m_suggestButton->setEnabled(false); ui.m_infoBrowser->setText(i18nc("@info","Error fetching the bug report")); ui.m_statusWidget->setIdle(i18nc("@info:status","Error fetching the bug report")); ui.m_retryButton->setVisible(true); } } void BugzillaReportInformationDialog::toggleShowOwnBacktrace(bool show) { QList sizes; if (show) { int size = (ui.m_reportSplitter->sizeHint().width()-ui.m_reportSplitter->handleWidth())/2; sizes << size << size; } else { sizes << ui.m_reportSplitter->sizeHint().width() << 0; //Hide backtrace } ui.m_reportSplitter->setSizes(sizes); //Save the current show value KConfigGroup config(KSharedConfig::openConfig(), "BugzillaReportInformationDialog"); config.writeEntry("ShowOwnBacktrace", show); } //END BugzillaReportInformationDialog //BEGIN BugzillaReportConfirmationDialog BugzillaReportConfirmationDialog::BugzillaReportConfirmationDialog(int bugNumber, bool commonCrash, QString closedState, BugzillaReportInformationDialog * parent) : QDialog(parent), m_parent(parent), m_showProceedQuestion(false), m_bugNumber(bugNumber) { setAttribute(Qt::WA_DeleteOnClose, true); setModal(true); ui.setupUi(this); //Setup dialog setWindowTitle(i18nc("@title:window", "Related Bug Report")); //Setup buttons ui.buttonBox->button(QDialogButtonBox::Cancel)->setText(i18nc("@action:button", "Cancel (Go back to the report)")); ui.buttonBox->button(QDialogButtonBox::Ok)->setText(i18nc("@action:button continue with the selected option " "and close the dialog", "Continue")); ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); connect(this, &BugzillaReportConfirmationDialog::accepted, this, &BugzillaReportConfirmationDialog::proceedClicked); connect(this, &BugzillaReportConfirmationDialog::rejected, this, &BugzillaReportConfirmationDialog::hide); //Set introduction text ui.introLabel->setText(i18n("You are going to mark your crash as related to bug %1", QString::number(m_bugNumber))); if (commonCrash) { //Common ("massive") crash m_showProceedQuestion = true; ui.commonCrashIcon->setPixmap(QIcon::fromTheme(QStringLiteral("edit-bomb")).pixmap(22,22)); } else { ui.commonCrashLabel->setVisible(false); ui.commonCrashIcon->setVisible(false); } if (!closedState.isEmpty()) { //Bug report closed ui.closedReportLabel->setText( i18nc("@info", "The report is closed because %1. " "If the crash is the same, adding further information will be useless " "and will consume developers' time.", closedState)); ui.closedReportIcon->setPixmap(QIcon::fromTheme(QStringLiteral("document-close")).pixmap(22,22)); m_showProceedQuestion = true; } else { ui.closedReportLabel->setVisible(false); ui.closedReportIcon->setVisible(false); } //Disable all the radio buttons ui.proceedRadioYes->setChecked(false); ui.proceedRadioNo->setChecked(false); ui.markAsDuplicateCheck->setChecked(false); ui.attachToBugReportCheck->setChecked(false); connect(ui.buttonGroupProceed, SIGNAL(buttonClicked(int)), this, SLOT(checkProceed())); connect(ui.buttonGroupProceedQuestion, SIGNAL(buttonClicked(int)), this, SLOT(checkProceed())); // Also listen to toggle so radio buttons are covered. connect(ui.buttonGroupProceed, static_cast(&QButtonGroup::buttonToggled), this, &BugzillaReportConfirmationDialog::checkProceed); connect(ui.buttonGroupProceedQuestion, static_cast(&QButtonGroup::buttonToggled), this, &BugzillaReportConfirmationDialog::checkProceed); if (!m_showProceedQuestion) { ui.proceedLabel->setEnabled(false); ui.proceedRadioYes->setEnabled(false); ui.proceedRadioNo->setEnabled(false); ui.proceedLabel->setVisible(false); ui.proceedRadioYes->setVisible(false); ui.proceedRadioNo->setVisible(false); ui.proceedRadioYes->setChecked(true); } checkProceed(); } BugzillaReportConfirmationDialog::~BugzillaReportConfirmationDialog() { } void BugzillaReportConfirmationDialog::checkProceed() { bool yes = ui.proceedRadioYes->isChecked(); bool no = ui.proceedRadioNo->isChecked(); //Enable/disable labels and controls ui.areYouSureLabel->setEnabled(yes); ui.markAsDuplicateCheck->setEnabled(yes); ui.attachToBugReportCheck->setEnabled(yes); //Enable Continue button if valid options are selected bool possibleDupe = ui.markAsDuplicateCheck->isChecked(); bool attach = ui.attachToBugReportCheck->isChecked(); bool enableContinueButton = yes ? (possibleDupe || attach) : no; ui.buttonBox->button(QDialogButtonBox::Ok)->setEnabled(enableContinueButton); } void BugzillaReportConfirmationDialog::proceedClicked() { if (ui.proceedRadioYes->isChecked()) { if (ui.markAsDuplicateCheck->isChecked()) { m_parent->markAsDuplicate(); hide(); } else { m_parent->attachToBugReport(); hide(); } } else { hide(); m_parent->cancelAssistant(); } } //END BugzillaReportConfirmationDialog diff --git a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h index 9b957ac8..cc3a93a7 100644 --- a/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h +++ b/src/bugzillaintegration/reportassistantpages_bugzilla_duplicates.h @@ -1,174 +1,173 @@ /******************************************************************* * reportassistantpages_bugzilla_duplicates.h * Copyright 2009 Dario Andres Rodriguez +* 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 * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #ifndef REPORTASSISTANTPAGES__BUGZILLA__DUPLICATES_H #define REPORTASSISTANTPAGES__BUGZILLA__DUPLICATES_H #include "reportassistantpage.h" #include "duplicatefinderjob.h" #include "bugzillalib.h" #include "ui_assistantpage_bugzilla_duplicates.h" #include "ui_assistantpage_bugzilla_duplicates_dialog.h" #include "ui_assistantpage_bugzilla_duplicates_dialog_confirmation.h" #include #include #include class QDate; class QTreeWidgetItem; class KGuiItem; class BugzillaReportInformationDialog; /** Searching for duplicates and showing report information page**/ class BugzillaDuplicatesPage : public ReportAssistantPage { Q_OBJECT public: explicit BugzillaDuplicatesPage(ReportAssistantDialog *); ~BugzillaDuplicatesPage() override; void aboutToShow() override; void aboutToHide() override; bool isComplete() override; bool showNextPage() override; 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); void itemClicked(QListWidgetItem *); void showReportInformationDialog(int, bool relatedButtonEnabled = true); void itemSelectionChanged(); /* Selected duplicates list related methods */ void addPossibleDuplicateNumber(int); void removeSelectedDuplicate(); void showDuplicatesPanel(bool); void possibleDuplicateSelectionChanged(); /* Attach to bug related methods */ void attachToBugReport(int); void cancelAttachToBugReport(); 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 **/ class BugzillaReportInformationDialog : public QDialog { Q_OBJECT public: explicit BugzillaReportInformationDialog(BugzillaDuplicatesPage*parent=nullptr); ~BugzillaReportInformationDialog() override; void showBugReport(int bugNumber, bool relatedButtonEnabled = true); void markAsDuplicate(); void attachToBugReport(); 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(); void relatedReportClicked(); void toggleShowOwnBacktrace(bool); Q_SIGNALS: void possibleDuplicateSelected(int); void attachToBugReportSelected(int); private: Ui::AssistantPageBugzillaDuplicatesDialog ui; bool m_relatedButtonEnabled; BugzillaDuplicatesPage * m_parent; int m_bugNumber; QString m_closedStateString; int m_duplicatesCount; QPushButton* m_suggestButton; + + Bugzilla::Bug::Ptr m_bug = nullptr; }; class BugzillaReportConfirmationDialog : public QDialog { Q_OBJECT public: BugzillaReportConfirmationDialog(int bugNumber, bool commonCrash, QString closedState, BugzillaReportInformationDialog * parent); ~BugzillaReportConfirmationDialog() override; private Q_SLOTS: void proceedClicked(); void checkProceed(); private: Ui::ConfirmationDialog ui; BugzillaReportInformationDialog * m_parent; bool m_showProceedQuestion; int m_bugNumber; }; #endif diff --git a/src/bugzillaintegration/reportinterface.cpp b/src/bugzillaintegration/reportinterface.cpp index f21017bb..54873479 100644 --- a/src/bugzillaintegration/reportinterface.cpp +++ b/src/bugzillaintegration/reportinterface.cpp @@ -1,448 +1,429 @@ /******************************************************************* * reportinterface.cpp * Copyright 2009,2010, 2011 Dario Andres Rodriguez * Copyright 2009 George Kiagiadakis * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include "reportinterface.h" #include #include "drkonqi.h" #include "bugzillalib.h" #include "productmapping.h" #include "systeminformation.h" #include "crashedapplication.h" #include "debuggermanager.h" #include "parser/backtraceparser.h" #include "backtracegenerator.h" #include "applicationdetailsexamples.h" ReportInterface::ReportInterface(QObject *parent) : QObject(parent), m_duplicate(0) { m_bugzillaManager = new BugzillaManager(KDE_BUGZILLA_URL, this); m_productMapping = new ProductMapping(DrKonqi::crashedApplication(), m_bugzillaManager, this); m_appDetailsExamples = new ApplicationDetailsExamples(this); //Information the user can provide about the crash m_userRememberCrashSituation = false; m_reproducible = ReproducibleUnsure; m_provideActionsApplicationDesktop = false; m_provideUnusualBehavior = false; m_provideApplicationConfigurationDetails = false; //Do not attach the bug report to any other existent report (create a new one) m_attachToBugNumber = 0; } void ReportInterface::setBugAwarenessPageData(bool rememberSituation, Reproducible reproducible, bool actions, bool unusual, bool configuration) { //Save the information the user can provide about the crash from the assistant page m_userRememberCrashSituation = rememberSituation; m_reproducible = reproducible; m_provideActionsApplicationDesktop = actions; m_provideUnusualBehavior = unusual; m_provideApplicationConfigurationDetails = configuration; } bool ReportInterface::isBugAwarenessPageDataUseful() const { //Determine if the assistant should proceed, considering the amount of information //the user can provide int rating = selectedOptionsRating(); //Minimum information required even for a good backtrace. bool useful = m_userRememberCrashSituation && (rating >= 2 || (m_reproducible==ReproducibleSometimes || m_reproducible==ReproducibleEverytime)); return useful; } int ReportInterface::selectedOptionsRating() const { //Check how many information the user can provide and generate a rating int rating = 0; if (m_provideActionsApplicationDesktop) { rating += 3; } if (m_provideApplicationConfigurationDetails) { rating += 2; } if (m_provideUnusualBehavior) { rating += 1; } return rating; } QString ReportInterface::backtrace() const { return m_backtrace; } void ReportInterface::setBacktrace(const QString & backtrace) { m_backtrace = backtrace; } QStringList ReportInterface::firstBacktraceFunctions() const { return m_firstBacktraceFunctions; } void ReportInterface::setFirstBacktraceFunctions(const QStringList & functions) { m_firstBacktraceFunctions = functions; } QString ReportInterface::title() const { return m_reportTitle; } void ReportInterface::setTitle(const QString & text) { m_reportTitle = text; } void ReportInterface::setDetailText(const QString & text) { m_reportDetailText = text; } void ReportInterface::setPossibleDuplicates(const QStringList & list) { m_possibleDuplicates = list; } QString ReportInterface::generateReportFullText(bool drKonqiStamp) const { //Note: no translations should be done in this function's strings const CrashedApplication * crashedApp = DrKonqi::crashedApplication(); const SystemInformation * sysInfo = DrKonqi::systemInformation(); QString report; //Program name and versions report.append(QStringLiteral("Application: %1 (%2)\n").arg(crashedApp->fakeExecutableBaseName(), crashedApp->version())); if ( sysInfo->compiledSources() ) { report.append(QStringLiteral(" (Compiled from sources)\n")); } else { report.append(QStringLiteral("\n")); } report.append(QStringLiteral("Qt Version: %1\n").arg(sysInfo->qtVersion())); report.append(QStringLiteral("Frameworks Version: %1\n").arg(sysInfo->frameworksVersion())); report.append(QStringLiteral("Operating System: %1\n").arg(sysInfo->operatingSystem())); //LSB output or manually selected distro if ( !sysInfo->lsbRelease().isEmpty() ) { report.append(QStringLiteral("Distribution: %1\n").arg(sysInfo->lsbRelease())); } else if ( !sysInfo->bugzillaPlatform().isEmpty() && sysInfo->bugzillaPlatform() != QLatin1String("unspecified")) { report.append(QStringLiteral("Distribution (Platform): %1\n").arg( sysInfo->bugzillaPlatform())); } report.append(QLatin1Char('\n')); //Details of the crash situation if (isBugAwarenessPageDataUseful()) { report.append(QStringLiteral("-- Information about the crash:\n")); if (!m_reportDetailText.isEmpty()) { report.append(m_reportDetailText.trimmed()); } else { //If the user manual reports this crash, he/she should know what to put in here. //This message is the only one translated in this function report.append(xi18nc("@info/plain","In detail, tell us what you were doing " " when the application crashed.")); } report.append(QLatin1String("\n\n")); } //Crash reproducibility (only if useful) if (m_reproducible != ReproducibleUnsure) { if (m_reproducible == ReproducibleEverytime) { report.append(QStringLiteral("The crash can be reproduced every time.\n\n")); } else if (m_reproducible == ReproducibleSometimes) { report.append(QStringLiteral("The crash can be reproduced sometimes.\n\n")); } else if (m_reproducible == ReproducibleNever) { report.append(QStringLiteral("The crash does not seem to be reproducible.\n\n")); } } //Backtrace report.append(QStringLiteral("-- Backtrace:\n")); if (!m_backtrace.isEmpty()) { QString formattedBacktrace = m_backtrace.trimmed(); report.append(formattedBacktrace + QLatin1Char('\n')); } else { report.append(QStringLiteral("A useful backtrace could not be generated\n")); } //Possible duplicates (selected by the user) if (!m_possibleDuplicates.isEmpty()) { report.append(QLatin1Char('\n')); QString duplicatesString; Q_FOREACH(const QString & dupe, m_possibleDuplicates) { duplicatesString += QLatin1String("bug ") + dupe + QLatin1String(", "); } duplicatesString = duplicatesString.left(duplicatesString.length()-2) + QLatin1Char('.'); report.append(QStringLiteral("The reporter indicates this bug may be a duplicate of or related to %1\n") .arg(duplicatesString)); } //Several possible duplicates (by bugzilla query) if (!m_allPossibleDuplicatesByQuery.isEmpty()) { report.append(QLatin1Char('\n')); QString duplicatesString; int count = m_allPossibleDuplicatesByQuery.count(); for(int i=0; i < count && i < 5; i++) { duplicatesString += QLatin1String("bug ") + m_allPossibleDuplicatesByQuery.at(i) + QLatin1String(", "); } duplicatesString = duplicatesString.left(duplicatesString.length()-2) + QLatin1Char('.'); report.append(QStringLiteral("Possible duplicates by query: %1\n").arg(duplicatesString)); } if (drKonqiStamp) { report.append(QLatin1String("\nReported using DrKonqi")); } return report; } QString ReportInterface::generateAttachmentComment() const { //Note: no translations should be done in this function's strings const CrashedApplication * crashedApp = DrKonqi::crashedApplication(); const SystemInformation * sysInfo = DrKonqi::systemInformation(); QString comment; //Program name and versions comment.append(QStringLiteral("%1 (%2) using Qt %4\n\n") .arg(crashedApp->fakeExecutableBaseName()) .arg(crashedApp->version()) .arg(sysInfo->qtVersion())); //Details of the crash situation if (isBugAwarenessPageDataUseful()) { comment.append(QStringLiteral("%1\n\n").arg(m_reportDetailText.trimmed())); } //Backtrace (only 6 lines) comment.append(QStringLiteral("-- Backtrace (Reduced):\n")); QString reducedBacktrace = DrKonqi::debuggerManager()->backtraceGenerator()->parser()->simplifiedBacktrace(); comment.append(reducedBacktrace.trimmed()); 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 { if (m_attachToBugNumber > 0) { //We are going to attach the report to an existent one connect(m_bugzillaManager, &BugzillaManager::addMeToCCFinished, this, &ReportInterface::addedToCC); connect(m_bugzillaManager, &BugzillaManager::addMeToCCError, this, &ReportInterface::sendReportError); //First add the user to the CC list, then attach 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); connect(m_bugzillaManager, &BugzillaManager::sendReportError, this, &ReportInterface::sendReportError); m_bugzillaManager->sendReport(report); } } void ReportInterface::sendUsingDefaultProduct() const { //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() { //The user was added to the CC list, proceed with the attachment connect(m_bugzillaManager, &BugzillaManager::attachToReportSent, this, &ReportInterface::attachSent); connect(m_bugzillaManager, &BugzillaManager::attachToReportError, this, &ReportInterface::sendReportError); QString reportText = generateReportFullText(true); QString comment = generateAttachmentComment(); QString filename = getSuggestedKCrashFilename(DrKonqi::crashedApplication()); QLatin1String summary("New crash information added by DrKonqi"); //Attach the report. The comment of the attachment also includes the bug description m_bugzillaManager->attachTextToReport(reportText, filename, summary, m_attachToBugNumber, comment); } void ReportInterface::attachSent(int attachId) { Q_UNUSED(attachId); //The bug was attached, consider it "sent" emit reportSent(m_attachToBugNumber); } QStringList ReportInterface::relatedBugzillaProducts() const { return m_productMapping->relatedBugzillaProducts(); } bool ReportInterface::isWorthReporting() const { if (DrKonqi::ignoreQuality()) { return true; } //Evaluate if the provided information is useful enough to enable the automatic report bool needToReport = false; if (!m_userRememberCrashSituation) { //This should never happen... but... return false; } int rating = selectedOptionsRating(); BacktraceParser::Usefulness use = DrKonqi::debuggerManager()->backtraceGenerator()->parser()->backtraceUsefulness(); switch (use) { case BacktraceParser::ReallyUseful: { //Perfect backtrace: require at least one option or a 100%-50% reproducible crash needToReport = (rating >=2) || (m_reproducible == ReproducibleEverytime || m_reproducible == ReproducibleSometimes); break; } case BacktraceParser::MayBeUseful: { //Not perfect backtrace: require at least two options or a 100% reproducible crash needToReport = (rating >=3) || (m_reproducible == ReproducibleEverytime); break; } case BacktraceParser::ProbablyUseless: //Bad backtrace: require at least two options and always reproducible (strict) needToReport = (rating >=5) && (m_reproducible == ReproducibleEverytime); break; case BacktraceParser::Useless: case BacktraceParser::InvalidUsefulness: { needToReport = false; } } return needToReport; } void ReportInterface::setAttachToBugNumber(uint bugNumber) { //If bugNumber>0, the report is going to be attached to bugNumber m_attachToBugNumber = bugNumber; } uint ReportInterface::attachToBugNumber() const { return m_attachToBugNumber; } void ReportInterface::setDuplicateId(uint duplicate) { m_duplicate = duplicate; } uint ReportInterface::duplicateId() const { return m_duplicate; } void ReportInterface::setPossibleDuplicatesByQuery(const QStringList & list) { m_allPossibleDuplicatesByQuery = list; } BugzillaManager * ReportInterface::bugzillaManager() const { return m_bugzillaManager; } ApplicationDetailsExamples * ReportInterface::appDetailsExamples() const { return m_appDetailsExamples; } diff --git a/src/bugzillaintegration/reportinterface.h b/src/bugzillaintegration/reportinterface.h index b6cacce0..97316311 100644 --- a/src/bugzillaintegration/reportinterface.h +++ b/src/bugzillaintegration/reportinterface.h @@ -1,130 +1,132 @@ /******************************************************************* * reportinterface.h * Copyright 2009, 2011 Dario Andres Rodriguez * Copyright 2009 George Kiagiadakis * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #ifndef REPORTINTERFACE__H #define REPORTINTERFACE__H #include #include -class BugReport; +namespace Bugzilla { +class NewBug; +} class BugzillaManager; class ProductMapping; class ApplicationDetailsExamples; class ReportInterface : public QObject { Q_OBJECT public: enum Reproducible { ReproducibleUnsure, ReproducibleNever, ReproducibleSometimes, ReproducibleEverytime }; explicit ReportInterface(QObject *parent = nullptr); void setBugAwarenessPageData(bool, Reproducible, bool, bool, bool); bool isBugAwarenessPageDataUseful() const; int selectedOptionsRating() const; QStringList firstBacktraceFunctions() const; void setFirstBacktraceFunctions(const QStringList & functions); QString backtrace() const; void setBacktrace(const QString & backtrace); QString title() const; void setTitle(const QString & text); void setDetailText(const QString & text); void setPossibleDuplicates(const QStringList & duplicatesList); QString generateReportFullText(bool drKonqiStamp) const; - BugReport newBugReportTemplate() const; + Bugzilla::NewBug newBugReportTemplate() const; void sendBugReport() const; QStringList relatedBugzillaProducts() const; bool isWorthReporting() const; //Zero means creating a new bug report void setAttachToBugNumber(uint); uint attachToBugNumber() const; //Zero means there is no duplicate void setDuplicateId(uint); uint duplicateId() const; void setPossibleDuplicatesByQuery(const QStringList &); BugzillaManager * bugzillaManager() const; ApplicationDetailsExamples * appDetailsExamples() const; bool userCanProvideActionsAppDesktop() const { return m_provideActionsApplicationDesktop; } bool userCanProvideUnusualBehavior() const { return m_provideUnusualBehavior; } bool userCanProvideApplicationConfigDetails() const { return m_provideApplicationConfigurationDetails; } private Q_SLOTS: void sendUsingDefaultProduct() const; void addedToCC(); void attachSent(int); Q_SIGNALS: void reportSent(int); void sendReportError(const QString &, const QString &); private: QString generateAttachmentComment() const; //Information the user can provide bool m_userRememberCrashSituation; Reproducible m_reproducible; bool m_provideActionsApplicationDesktop; bool m_provideUnusualBehavior; bool m_provideApplicationConfigurationDetails; QString m_backtrace; QStringList m_firstBacktraceFunctions; QString m_reportTitle; QString m_reportDetailText; QStringList m_possibleDuplicates; QStringList m_allPossibleDuplicatesByQuery; uint m_attachToBugNumber; uint m_duplicate; ProductMapping * m_productMapping = nullptr; BugzillaManager * m_bugzillaManager = nullptr; ApplicationDetailsExamples * m_appDetailsExamples = nullptr; }; #endif diff --git a/src/drkonqi.cpp b/src/drkonqi.cpp index 35c9b3d6..5b2527fa 100644 --- a/src/drkonqi.cpp +++ b/src/drkonqi.cpp @@ -1,358 +1,368 @@ /* Copyright (C) 2009 George Kiagiadakis This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Parts of this code were originally under the following license: * Copyright (C) 2000-2003 Hans Petter Bieker * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "drkonqi.h" #include "drkonqi_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include "systeminformation.h" #include "crashedapplication.h" #include "drkonqibackends.h" DrKonqi::DrKonqi() : m_signal(0) , m_pid(0) , m_kdeinit(false) , m_safer(false) , m_restarted(false) , m_keepRunning(false) , m_thread(0) { m_backend = new KCrashBackend(); m_systemInformation = new SystemInformation(); } DrKonqi::~DrKonqi() { delete m_systemInformation; delete m_backend; } //static DrKonqi *DrKonqi::instance() { static DrKonqi *drKonqiInstance = nullptr; if (!drKonqiInstance) { drKonqiInstance = new DrKonqi(); } return drKonqiInstance; } //based on KCrashDelaySetHandler from kdeui/util/kcrash.cpp class EnableCrashCatchingDelayed : public QObject { public: EnableCrashCatchingDelayed() { startTimer(10000); // 10 s } protected: void timerEvent(QTimerEvent *event) override { qCDebug(DRKONQI_LOG) << "Enabling drkonqi crash catching"; KCrash::setDrKonqiEnabled(true); killTimer(event->timerId()); this->deleteLater(); } }; bool DrKonqi::init() { if (!instance()->m_backend->init()) { cleanup(); return false; } else { //all ok, continue initialization // Set drkonqi to handle its own crashes, but only if the crashed app // is not drkonqi already. If it is drkonqi, delay enabling crash catching // to prevent recursive crashes (in case it crashes at startup) if (crashedApplication()->fakeExecutableBaseName() != QLatin1String("drkonqi")) { qCDebug(DRKONQI_LOG) << "Enabling drkonqi crash catching"; KCrash::setDrKonqiEnabled(true); } else { new EnableCrashCatchingDelayed; } return true; } } void DrKonqi::cleanup() { delete instance(); } //static SystemInformation *DrKonqi::systemInformation() { return instance()->m_systemInformation; } //static DebuggerManager* DrKonqi::debuggerManager() { return instance()->m_backend->debuggerManager(); } //static CrashedApplication *DrKonqi::crashedApplication() { return instance()->m_backend->crashedApplication(); } //static void DrKonqi::saveReport(const QString & reportText, QWidget *parent) { if (isSafer()) { QTemporaryFile tf; tf.setFileTemplate(QStringLiteral("XXXXXX.kcrash.txt")); tf.setAutoRemove(false); if (tf.open()) { QTextStream textStream(&tf); textStream << reportText; textStream.flush(); KMessageBox::information(parent, xi18nc("@info", "Report saved to %1.", tf.fileName())); } else { KMessageBox::sorry(parent, i18nc("@info","Could not create a file in which to save the report.")); } } else { QString defname = getSuggestedKCrashFilename(crashedApplication()); QPointer dlg(new QFileDialog(parent, defname)); dlg->selectFile(defname); dlg->setWindowTitle(i18nc("@title:window","Select Filename")); dlg->setAcceptMode(QFileDialog::AcceptSave); dlg->setFileMode(QFileDialog::AnyFile); dlg->setConfirmOverwrite(true); if (dlg->exec() != QDialog::Accepted) { return; } if (!dlg) { //Dialog is invalid, it was probably deleted (ex. via DBus call) //return and do not crash return; } QUrl fileUrl; if(!dlg->selectedUrls().isEmpty()) fileUrl = dlg->selectedUrls().first(); delete dlg; if (fileUrl.isValid()) { QTemporaryFile tf; if (tf.open()) { QTextStream ts(&tf); ts << reportText; ts.flush(); } else { KMessageBox::sorry(parent, xi18nc("@info","Cannot open file %1 " "for writing.", tf.fileName())); return; } // QFileDialog was run with confirmOverwrite, so we can safely // overwrite as necesssary. KIO::FileCopyJob* job = KIO::file_copy(QUrl::fromLocalFile(tf.fileName()), fileUrl, -1, KIO::DefaultFlags | KIO::Overwrite); KJobWidgets::setWindow(job, parent); if (!job->exec()) { KMessageBox::sorry(parent, job->errorString()); } } } } void DrKonqi::setSignal(int signal) { instance()->m_signal = signal; } void DrKonqi::setAppName(const QString &appName) { instance()->m_appName = appName; } void DrKonqi::setAppPath(const QString &appPath) { instance()->m_appPath = appPath; } void DrKonqi::setAppVersion(const QString &appVersion) { instance()->m_appVersion = appVersion; } void DrKonqi::setBugAddress(const QString &bugAddress) { instance()->m_bugAddress = bugAddress; } void DrKonqi::setProgramName(const QString &programName) { instance()->m_programName = programName; } void DrKonqi::setPid(int pid) { instance()->m_pid = pid; } void DrKonqi::setKdeinit(bool kdeinit) { instance()->m_kdeinit = kdeinit; } void DrKonqi::setSafer(bool safer) { instance()->m_safer = safer; } void DrKonqi::setRestarted(bool restarted) { instance()->m_restarted = restarted; } void DrKonqi::setKeepRunning(bool keepRunning) { instance()->m_keepRunning = keepRunning; } void DrKonqi::setThread(int thread) { instance()->m_thread = thread; } int DrKonqi::signal() { return instance()->m_signal; } const QString &DrKonqi::appName() { return instance()->m_appName; } const QString &DrKonqi::appPath() { return instance()->m_appPath; } const QString &DrKonqi::appVersion() { return instance()->m_appVersion; } const QString &DrKonqi::bugAddress() { return instance()->m_bugAddress; } const QString &DrKonqi::programName() { return instance()->m_programName; } int DrKonqi::pid() { return instance()->m_pid; } bool DrKonqi::isKdeinit() { return instance()->m_kdeinit; } bool DrKonqi::isSafer() { return instance()->m_safer; } bool DrKonqi::isRestarted() { return instance()->m_restarted; } bool DrKonqi::isKeepRunning() { return instance()->m_keepRunning; } int DrKonqi::thread() { return instance()->m_thread; } 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; } url = QString::fromLocal8Bit(qgetenv("DRKONQI_KDE_BUGZILLA_URL")); if (!url.isEmpty()) { 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; } diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 83c0da92..3174622b 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,50 +1,66 @@ +if(NOT BUILD_TESTING) + # Skip everything. Particularly trying to look for integration test deps. + return() +endif() + +if(BUILD_COVERAGE) + 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 + ) +endif() + add_subdirectory(crashtest) add_subdirectory(backtraceparsertest) -if(KF5XmlRpcClient_FOUND) - add_subdirectory(bugzillalibtest) -endif() +add_subdirectory(bugzillalibtest) if(NOT APPLE) if(NOT RUBY_EXECTUABLE) find_program(RUBY_EXECUTABLE ruby) endif() if(RUBY_EXECUTABLE) execute_process(COMMAND ${RUBY_EXECUTABLE} -e "require 'atspi'" RESULT_VARIABLE RUBY_ATSPI) - execute_process(COMMAND ${RUBY_EXECUTABLE} -e "require 'xmlrpc/server'" - RESULT_VARIABLE RUBY_XMLRPC) endif() if(NOT GDB_EXECUTABLE) # Needed so drkonqi can actually trace something. find_program(GDB_EXECUTABLE gdb) endif() if(NOT XVFB_RUN_EXECTUABLE) find_program(XVFB_RUN_EXECTUABLE xvfb-run) endif() set(ATSPI_PATHS /usr/lib/at-spi2-core/ # debians /usr/lib/at-spi2/ # suses ) if(NOT ATSPI_BUS_LAUNCHER_EXECUTABLE) find_program(ATSPI_BUS_LAUNCHER_EXECUTABLE NAMES at-spi-bus-launcher PATHS ${ATSPI_PATHS} DOC "AT-SPI accessibility dbus launcher") endif() if(NOT ATSPI_REGISTRYD_EXECUTABLE) find_program(ATSPI_REGISTRYD_EXECUTABLE NAMES at-spi2-registryd PATHS ${ATSPI_PATHS} DOC "AT-SPI accessibility registry daemon") endif() if(RUBY_EXECUTABLE AND XVFB_RUN_EXECTUABLE AND ATSPI_BUS_LAUNCHER_EXECUTABLE AND ATSPI_REGISTRYD_EXECUTABLE AND GDB_EXECUTABLE - AND RUBY_ATSPI EQUAL 0 AND RUBY_XMLRPC EQUAL 0) + AND RUBY_ATSPI EQUAL 0) set(WITH_DRKONI_INTEGRATION_TESTING TRUE) add_subdirectory(integration) endif() add_feature_info(DrKonqiIntegrationTesting WITH_DRKONI_INTEGRATION_TESTING - "Needs Ruby, functional atspi and xmlrpc gems, gdb, as well as xvfb-run.") + "Needs Ruby, functional atspi gem, gdb, as well as xvfb-run.") endif() diff --git a/src/tests/bugzillalibtest/CMakeLists.txt b/src/tests/bugzillalibtest/CMakeLists.txt index fade27a1..3087e99e 100644 --- a/src/tests/bugzillalibtest/CMakeLists.txt +++ b/src/tests/bugzillalibtest/CMakeLists.txt @@ -1,13 +1,11 @@ - set(bugzillalibtest_SRCS bugzillalibtest.cpp ../../bugzillaintegration/bugzillalib.cpp ) ecm_qt_declare_logging_category(bugzillalibtest_SRCS HEADER drkonqi_debug.h IDENTIFIER DRKONQI_LOG CATEGORY_NAME org.kde.drkonqi) add_executable(bugzillalibtest ${bugzillalibtest_SRCS}) ecm_mark_as_test(bugzillalibtest) -target_link_libraries(bugzillalibtest KF5::KIOWidgets KF5::XmlRpcClient Qt5::Xml KF5::I18n) - +target_link_libraries(bugzillalibtest KF5::KIOWidgets KF5::I18n qbugzilla) diff --git a/src/tests/bugzillalibtest/bugzillalibtest.cpp b/src/tests/bugzillalibtest/bugzillalibtest.cpp index 833c62f0..aa088f63 100644 --- a/src/tests/bugzillalibtest/bugzillalibtest.cpp +++ b/src/tests/bugzillalibtest/bugzillalibtest.cpp @@ -1,151 +1,149 @@ /******************************************************************* * bugzillalibtest.cpp * Copyright 2009 Dario Andres Rodriguez * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License as * published by the Free Software Foundation; either version 2 of * the License, or (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * ******************************************************************/ #include #include #include #include #include #include "../../bugzillaintegration/bugzillalib.h" #include "../../debugpackageinstaller.h" class BugzillaLibTest : public QObject { Q_OBJECT public: BugzillaLibTest(QString user, QString password) : QObject() { manager = new BugzillaManager(QStringLiteral("http://bugstest.kde.org/")); connect(manager, &BugzillaManager::loginFinished, this, &BugzillaLibTest::loginFinished); connect(manager, &BugzillaManager::loginError, this, &BugzillaLibTest::loginError); connect(manager, &BugzillaManager::reportSent, this, &BugzillaLibTest::reportSent); connect(manager, &BugzillaManager::sendReportError, this, &BugzillaLibTest::sendReportError); connect(manager, &BugzillaManager::sendReportErrorInvalidValues, this, &BugzillaLibTest::sendBR2); manager->tryLogin(user, password); qDebug() << "Login ..."; } private Q_SLOTS: void loginFinished(bool ok) { qDebug() << "Login Finished" << ok; if (!ok) { return; } //Uncomment to Test //FIXME provide a way to select the test from the command line //Send a new bug report /* sendBR(); */ //Attach a simple text to a report as a file /* manager->attachTextToReport("Bugzilla Lib Attachment Content Test", "/tmp/var", "Bugzilla Lib Attachment Description Test", 150000); */ /* manager->addMeToCC(100005); */ } void loginError(const QString & msg) { qDebug() << "Login Error" << msg; } void sendBR() { - BugReport br; - br.setValid(true); - br.setProduct(QStringLiteral("konqueror")); - br.setComponent(QStringLiteral("general")); - br.setVersion(QStringLiteral("undefined")); - br.setOperatingSystem(QStringLiteral("Linux")); - br.setPriority(QStringLiteral("NOR")); - br.setPlatform(QStringLiteral("random test")); - br.setBugSeverity(QStringLiteral("crash")); - br.setShortDescription(QStringLiteral("bla bla")); - br.setDescription(QStringLiteral("bla bla large")); + Bugzilla::NewBug br; + br.product = QStringLiteral("konqueror"); + br.component = QStringLiteral("general"); + br.version = QStringLiteral("undefined"); + br.op_sys = QStringLiteral("Linux"); + br.priority = QStringLiteral("NOR"); + br.platform = QStringLiteral("random test"); + br.severity = QStringLiteral("crash"); + br.summary = QStringLiteral("bla bla"); + br.description = QStringLiteral("bla bla large"); manager->sendReport(br); qDebug() << "Trying to send bug report"; } void sendBR2() { - BugReport br; - br.setValid(true); - br.setProduct(QStringLiteral("konqueror")); - br.setComponent(QStringLiteral("general")); - br.setVersion(QStringLiteral("undefined")); - br.setOperatingSystem(QStringLiteral("Linux")); - br.setPriority(QStringLiteral("NOR")); - br.setPlatform(QStringLiteral("unspecified")); - br.setBugSeverity(QStringLiteral("crash")); - br.setShortDescription(QStringLiteral("bla bla")); - br.setDescription(QStringLiteral("bla bla large")); + Bugzilla::NewBug br; + br.product = QStringLiteral("konqueror"); + br.component = QStringLiteral("general"); + br.version = QStringLiteral("undefined"); + br.op_sys = QStringLiteral("Linux"); + br.priority = QStringLiteral("NOR"); + br.platform = QStringLiteral("unspecified"); + br.severity = QStringLiteral("crash"); + br.summary = QStringLiteral("bla bla"); + br.description = QStringLiteral("bla bla large"); manager->sendReport(br); qDebug() << "Trying to send bug report"; } void reportSent( int num) { qDebug() << "BR sent " << num << manager->urlForBug(num); } void sendReportError(const QString & msg) { qDebug() << "Error sending bug report" << msg; } private: BugzillaManager * manager; }; int main (int argc, char ** argv) { QApplication app(argc, argv); KAboutData aboutData( QStringLiteral("bzlibtest"), i18n("BugzillaLib Test (DrKonqi2)"), QStringLiteral("1.0"), i18n("Test application for bugtracker manager lib"), KAboutLicense::GPL, i18n("(c) 2009, DrKonqi2 Developers")); QCommandLineParser parser; parser.addOption(QCommandLineOption(QStringLiteral("user"), i18nc("@info:shell","bugstest.kde.org username"), QStringLiteral("username"))); parser.addOption(QCommandLineOption(QStringLiteral("pass"), i18nc("@info:shell","bugstest.kde.org password"), QStringLiteral("password"))); aboutData.setupCommandLine(&parser); parser.process(app); aboutData.processCommandLine(&parser); if (!parser.isSet(QStringLiteral("user")) || !parser.isSet(QStringLiteral("pass"))) { qDebug() << "Provide bugstest.kde.org username and password. See help"; return 0; } new BugzillaLibTest(parser.value(QStringLiteral("user")), parser.value(QStringLiteral("pass"))); return app.exec(); } #include "bugzillalibtest.moc" diff --git a/src/tests/integration/data/bugs b/src/tests/integration/data/bugs new file mode 100644 index 00000000..d2c0299d --- /dev/null +++ b/src/tests/integration/data/bugs @@ -0,0 +1,67 @@ +{ + "bugs" : [ + { + "alias" : [], + "assigned_to" : "dolphin-bugs-null@kde.org", + "assigned_to_detail" : { + "email" : "dolphin-bugs-null@kde.org", + "id" : 168329, + "name" : "dolphin-bugs-null@kde.org", + "real_name" : "Dolphin Bug Assignee" + }, + "blocks" : [], + "cc" : [ + "elvis.angelaccio@kde.org" + ], + "cc_detail" : [ + { + "email" : "elvis.angelaccio@kde.org", + "id" : 183313, + "name" : "elvis.angelaccio@kde.org", + "real_name" : "Elvis Angelaccio" + } + ], + "cf_commitlink" : "", + "cf_versionfixedin" : "", + "classification" : "Unclassified", + "component" : "general", + "creation_time" : "2017-01-16T21:57:27Z", + "creator" : "j.peter0123@gmail.com", + "creator_detail" : { + "email" : "j.peter0123@gmail.com", + "id" : 199606, + "name" : "j.peter0123@gmail.com", + "real_name" : "Jung Péter" + }, + "deadline" : null, + "depends_on" : [], + "dupe_of" : null, + "flags" : [], + "groups" : [], + "id" : 375161, + "is_cc_accessible" : null, + "is_confirmed" : null, + "is_creator_accessible" : null, + "is_open" : null, + "keywords" : [ + "drkonqi" + ], + "last_change_time" : "2017-10-28T09:58:26Z", + "op_sys" : "Linux", + "platform" : "openSUSE RPMs", + "priority" : "NOR", + "product" : "dolphin", + "qa_contact" : "", + "resolution" : "", + "see_also" : [], + "severity" : "crash", + "status" : "UNCONFIRMED", + "summary" : "Dolphin crash, copy from Samba share", + "target_milestone" : "---", + "url" : "", + "version" : "16.08.2", + "whiteboard" : "" + } + ], + "faults" : [] +} diff --git a/src/tests/integration/data/comments b/src/tests/integration/data/comments new file mode 100644 index 00000000..407e12a0 --- /dev/null +++ b/src/tests/integration/data/comments @@ -0,0 +1,48 @@ + + +{ + "bugs" : { + "375161" : { + "comments" : [ + { + "attachment_id" : null, + "bug_id" : 375161, + "count" : 0, + "creation_time" : "2017-01-16T21:57:27Z", + "creator" : "j.peter0123@gmail.com", + "id" : 1654770, + "is_private" : null, + "tags" : [], + "text" : "Application: dolphin (16.08.2)\n\nQt Version: 5.6.1\nFrameworks Version: 5.26.0\nOperating System: Linux 4.4.36-8-default x86_64\nDistribution: \"openSUSE Leap 42.2\"\n\n-- Information about the crash:\n- What I was doing when the application crashed:\nI had copyed files from Samba share (with drag and drop methode).\n\n-- Backtrace:\nApplication: Dolphin (dolphin), signal: Segmentation fault\nUsing host libthread_db library \"/lib64/libthread_db.so.1\".\n[Current thread is 1 (Thread 0x7f03846b5900 (LWP 4820))]\n\nThread 4 (Thread 0x7f035bfff700 (LWP 4823)):\n#0 0x00007f0383f8951d in read () at /lib64/libc.so.6\n#1 0x00007f037430b073 in () at /usr/lib64/tls/libnvidia-tls.so.375.26\n#2 0x00007f0378dbb670 in () at /usr/lib64/libglib-2.0.so.0\n#3 0x00007f0378d7ae49 in g_main_context_check () at /usr/lib64/libglib-2.0.so.0\n#4 0x00007f0378d7b2a8 in () at /usr/lib64/libglib-2.0.so.0\n#5 0x00007f0378d7b42c in g_main_context_iteration () at /usr/lib64/libglib-2.0.so.0\n#6 0x00007f037e26932b in QEventDispatcherGlib::processEvents(QFlags) () at /usr/lib64/libQt5Core.so.5\n#7 0x00007f037e216fdb in QEventLoop::exec(QFlags) () at /usr/lib64/libQt5Core.so.5\n#8 0x00007f037e051f1a in QThread::exec() () at /usr/lib64/libQt5Core.so.5\n#9 0x00007f037e0569e9 in () at /usr/lib64/libQt5Core.so.5\n#10 0x00007f0379902734 in start_thread () at /lib64/libpthread.so.0\n#11 0x00007f0383f95d3d in clone () at /lib64/libc.so.6\n\nThread 3 (Thread 0x7f036a0f2700 (LWP 4822)):\n#0 0x00007f0383f8951d in read () at /lib64/libc.so.6\n#1 0x00007f037430b073 in () at /usr/lib64/tls/libnvidia-tls.so.375.26\n#2 0x00007f0378dbb670 in () at /usr/lib64/libglib-2.0.so.0\n#3 0x00007f0378d7ae49 in g_main_context_check () at /usr/lib64/libglib-2.0.so.0\n#4 0x00007f0378d7b2a8 in () at /usr/lib64/libglib-2.0.so.0\n#5 0x00007f0378d7b42c in g_main_context_iteration () at /usr/lib64/libglib-2.0.so.0\n#6 0x00007f037e26932b in QEventDispatcherGlib::processEvents(QFlags) () at /usr/lib64/libQt5Core.so.5\n#7 0x00007f037e216fdb in QEventLoop::exec(QFlags) () at /usr/lib64/libQt5Core.so.5\n#8 0x00007f037e051f1a in QThread::exec() () at /usr/lib64/libQt5Core.so.5\n#9 0x00007f037e67d1d5 in () at /usr/lib64/libQt5DBus.so.5\n#10 0x00007f037e0569e9 in () at /usr/lib64/libQt5Core.so.5\n#11 0x00007f0379902734 in start_thread () at /lib64/libpthread.so.0\n#12 0x00007f0383f95d3d in clone () at /lib64/libc.so.6\n\nThread 2 (Thread 0x7f036c09b700 (LWP 4821)):\n#0 0x00007f0383f8d49d in poll () at /lib64/libc.so.6\n#1 0x00007f0376c703e2 in () at /usr/lib64/libxcb.so.1\n#2 0x00007f0376c71fcf in xcb_wait_for_event () at /usr/lib64/libxcb.so.1\n#3 0x00007f036ec27839 in () at /usr/lib64/libQt5XcbQpa.so.5\n#4 0x00007f037e0569e9 in () at /usr/lib64/libQt5Core.so.5\n#5 0x00007f0379902734 in start_thread () at /lib64/libpthread.so.0\n#6 0x00007f0383f95d3d in clone () at /lib64/libc.so.6\n\nThread 1 (Thread 0x7f03846b5900 (LWP 4820)):\n[KCrash Handler]\n#6 0x00007f0383f28296 in malloc_usable_size () at /lib64/libc.so.6\n#7 0x00007f03783d5139 in () at /usr/X11R6/lib64/libGL.so.1\n#8 0x00007f037430b1fc in () at /usr/lib64/tls/libnvidia-tls.so.375.26\n#9 0x00007f037e2414a4 in QObjectPrivate::Connection::~Connection() () at /usr/lib64/libQt5Core.so.5\n#10 0x00007f037e246dcc in QObjectPrivate::cleanConnectionLists() () at /usr/lib64/libQt5Core.so.5\n#11 0x00007f037e246eab in QObjectPrivate::addConnection(int, QObjectPrivate::Connection*) () at /usr/lib64/libQt5Core.so.5\n#12 0x00007f037e2475c6 in () at /usr/lib64/libQt5Core.so.5\n#13 0x00007f037e247d57 in QObject::connect(QObject const*, char const*, QObject const*, char const*, Qt::ConnectionType) () at /usr/lib64/libQt5Core.so.5\n#14 0x00007f038207b54d in () at /usr/lib64/libKF5KIOCore.so.5\n#15 0x00007f038208c686 in () at /usr/lib64/libKF5KIOCore.so.5\n#16 0x00007f037e242bb1 in QMetaObject::activate(QObject*, int, int, void**) () at /usr/lib64/libQt5Core.so.5\n#17 0x00007f037e250112 in QTimer::timerEvent(QTimerEvent*) () at /usr/lib64/libQt5Core.so.5\n#18 0x00007f037e243f34 in QObject::event(QEvent*) () at /usr/lib64/libQt5Core.so.5\n#19 0x00007f037f1a0e3c in QApplicationPrivate::notify_helper(QObject*, QEvent*) () at /usr/lib64/libQt5Widgets.so.5\n#20 0x00007f037f1a549a in QApplication::notify(QObject*, QEvent*) () at /usr/lib64/libQt5Widgets.so.5\n#21 0x00007f037e218fc5 in QCoreApplication::notifyInternal2(QObject*, QEvent*) () at /usr/lib64/libQt5Core.so.5\n#22 0x00007f037e268c7e in QTimerInfoList::activateTimers() () at /usr/lib64/libQt5Core.so.5\n#23 0x00007f037e269079 in () at /usr/lib64/libQt5Core.so.5\n#24 0x00007f0378d7b134 in g_main_context_dispatch () at /usr/lib64/libglib-2.0.so.0\n#25 0x00007f0378d7b388 in () at /usr/lib64/libglib-2.0.so.0\n#26 0x00007f0378d7b42c in g_main_context_iteration () at /usr/lib64/libglib-2.0.so.0\n#27 0x00007f037e26930c in QEventDispatcherGlib::processEvents(QFlags) () at /usr/lib64/libQt5Core.so.5\n#28 0x00007f037e216fdb in QEventLoop::exec(QFlags) () at /usr/lib64/libQt5Core.so.5\n#29 0x00007f037e21eec6 in QCoreApplication::exec() () at /usr/lib64/libQt5Core.so.5\n#30 0x00007f03842b196a in kdemain () at /usr/lib64/libkdeinit5_dolphin.so\n#31 0x00007f0383ecc6e5 in __libc_start_main () at /lib64/libc.so.6\n#32 0x0000000000400789 in _start ()\n\nReported using DrKonqi", + "time" : "2017-01-16T21:57:27Z" + }, + { + "attachment_id" : null, + "bug_id" : 375161, + "count" : 1, + "creation_time" : "2017-01-16T22:45:08Z", + "creator" : "elvis.angelaccio@kde.org", + "id" : 1654785, + "is_private" : null, + "tags" : [], + "text" : "Thanks for the report. Unfortunately the backtracke is not very useful. If you can reproduce the crash, please install debug symbols and attach a new backtrace here. See also https://community.kde.org/Dolphin/FAQ/Crashes", + "time" : "2017-01-16T22:45:08Z" + }, + { + "attachment_id" : null, + "bug_id" : 375161, + "count" : 2, + "creation_time" : "2017-10-28T09:58:26Z", + "creator" : "elvis.angelaccio@kde.org", + "id" : 1708610, + "is_private" : null, + "tags" : [], + "text" : "\n\n*** This bug has been marked as a duplicate of bug 386277 ***", + "time" : "2017-10-28T09:58:26Z" + } + ] + } + }, + "comments" : {} +} + diff --git a/src/tests/integration/data/product.dolphin b/src/tests/integration/data/product.dolphin new file mode 100644 index 00000000..1ba00cf8 --- /dev/null +++ b/src/tests/integration/data/product.dolphin @@ -0,0 +1,2354 @@ + + +{ + "products" : [ + { + "classification" : "Unclassified", + "components" : [ + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Filter Bar", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 2107, + "is_active" : null, + "name" : "bars: filter", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Location Bar", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 2105, + "is_active" : null, + "name" : "bars: location", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Status Bar", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 2106, + "is_active" : null, + "name" : "bars: status", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs that do not belong to 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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1072, + "is_active" : null, + "name" : "general", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Folders Panel", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1815, + "is_active" : null, + "name" : "panels: folders", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Information Panel", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1816, + "is_active" : null, + "name" : "panels: information", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Places Panel", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1817, + "is_active" : null, + "name" : "panels: places", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Search Panel", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1814, + "is_active" : null, + "name" : "panels: search", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Terminal Panel", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1818, + "is_active" : null, + "name" : "panels: terminal", + "sort_key" : 0 + }, + { + "default_assigned_to" : "jr@jriddell.org", + "default_qa_contact" : "", + "description" : "Version control plugin for Bazaar", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1942, + "is_active" : null, + "name" : "plugins: bazaar", + "sort_key" : 0 + }, + { + "default_assigned_to" : "emmanuelpescosta099@gmail.com", + "default_qa_contact" : "", + "description" : "Dropbox Plugin", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1825, + "is_active" : null, + "name" : "plugins: dropbox", + "sort_key" : 0 + }, + { + "default_assigned_to" : "sebastian@sebastian-doerner.de", + "default_qa_contact" : "", + "description" : "Git Plugin", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1824, + "is_active" : null, + "name" : "plugins: git", + "sort_key" : 0 + }, + { + "default_assigned_to" : "vishesh3y@yahoo.com", + "default_qa_contact" : "", + "description" : "Mercurial Plugin", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1974, + "is_active" : null, + "name" : "plugins: mercurial", + "sort_key" : 0 + }, + { + "default_assigned_to" : "sebastian@sebastian-doerner.de", + "default_qa_contact" : "", + "description" : "Subversion Plugin", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1823, + "is_active" : null, + "name" : "plugins: svn", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs related to searching of files or metadata", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1819, + "is_active" : null, + "name" : "search", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "The split view feature", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 2108, + "is_active" : null, + "name" : "split view", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs for the view mode Columns", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1822, + "is_active" : null, + "name" : "view-engine: columns mode", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs for the view mode Compact", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1989, + "is_active" : null, + "name" : "view-engine: compact mode", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs for the view mode Details", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1821, + "is_active" : null, + "name" : "view-engine: details mode", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "General view-engine issue that are not related to the view-mode", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1982, + "is_active" : null, + "name" : "view-engine: general", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Bugs for the view mode Icons", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 1820, + "is_active" : null, + "name" : "view-engine: icons mode", + "sort_key" : 0 + }, + { + "default_assigned_to" : "dolphin-bugs-null@kde.org", + "default_qa_contact" : "", + "description" : "Tooltip", + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "corner_case", + "request_group" : null, + "sort_key" : 1 + }, + { + "cc_list" : "", + "description" : "the strings are not translatable", + "grant_group" : 6, + "id" : 20, + "is_active" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "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" : null, + "is_multiplicable" : null, + "is_requestable" : null, + "is_requesteeble" : null, + "name" : "Usability", + "request_group" : 6, + "sort_key" : 1 + } + ] + }, + "id" : 2109, + "is_active" : null, + "name" : "view-engine: tooltip", + "sort_key" : 0 + } + ], + "default_milestone" : "---", + "description" : "File manager", + "has_unconfirmed" : null, + "id" : 371, + "is_active" : null, + "milestones" : [ + { + "id" : 371, + "is_active" : null, + "name" : "---", + "sort_key" : 0 + } + ], + "name" : "dolphin", + "versions" : [ + { + "id" : 6326, + "is_active" : null, + "name" : "1.3", + "sort_key" : 0 + }, + { + "id" : 6327, + "is_active" : null, + "name" : "1.4", + "sort_key" : 0 + }, + { + "id" : 6328, + "is_active" : null, + "name" : "1.5", + "sort_key" : 0 + }, + { + "id" : 4407, + "is_active" : null, + "name" : "1.6.1", + "sort_key" : 0 + }, + { + "id" : 4516, + "is_active" : null, + "name" : "1.7", + "sort_key" : 0 + }, + { + "id" : 4536, + "is_active" : null, + "name" : "1.99", + "sort_key" : 0 + }, + { + "id" : 4681, + "is_active" : null, + "name" : "2.0", + "sort_key" : 0 + }, + { + "id" : 4785, + "is_active" : null, + "name" : "2.0.95", + "sort_key" : 0 + }, + { + "id" : 5033, + "is_active" : null, + "name" : "2.1", + "sort_key" : 0 + }, + { + "id" : 5462, + "is_active" : null, + "name" : "2.1.80", + "sort_key" : 0 + }, + { + "id" : 5504, + "is_active" : null, + "name" : "2.1.85", + "sort_key" : 0 + }, + { + "id" : 5605, + "is_active" : null, + "name" : "2.1.97", + "sort_key" : 0 + }, + { + "id" : 5711, + "is_active" : null, + "name" : "2.2", + "sort_key" : 0 + }, + { + "id" : 6666, + "is_active" : null, + "name" : "2.2.60", + "sort_key" : 0 + }, + { + "id" : 8071, + "is_active" : null, + "name" : "4.9.5", + "sort_key" : 0 + }, + { + "id" : 6782, + "is_active" : null, + "name" : "4.10.5", + "sort_key" : 0 + }, + { + "id" : 6745, + "is_active" : null, + "name" : "4.10.90", + "sort_key" : 0 + }, + { + "id" : 6876, + "is_active" : null, + "name" : "4.10.95", + "sort_key" : 0 + }, + { + "id" : 6909, + "is_active" : null, + "name" : "4.10.97", + "sort_key" : 0 + }, + { + "id" : 7065, + "is_active" : null, + "name" : "4.11.0", + "sort_key" : 0 + }, + { + "id" : 7177, + "is_active" : null, + "name" : "4.11.1", + "sort_key" : 0 + }, + { + "id" : 7263, + "is_active" : null, + "name" : "4.11.2", + "sort_key" : 0 + }, + { + "id" : 7370, + "is_active" : null, + "name" : "4.11.3", + "sort_key" : 0 + }, + { + "id" : 7684, + "is_active" : null, + "name" : "4.11.4", + "sort_key" : 0 + }, + { + "id" : 7951, + "is_active" : null, + "name" : "4.11.5", + "sort_key" : 0 + }, + { + "id" : 7027, + "is_active" : null, + "name" : "4.11.60", + "sort_key" : 0 + }, + { + "id" : 7446, + "is_active" : null, + "name" : "4.11.80", + "sort_key" : 0 + }, + { + "id" : 7507, + "is_active" : null, + "name" : "4.11.90", + "sort_key" : 0 + }, + { + "id" : 7561, + "is_active" : null, + "name" : "4.11.95", + "sort_key" : 0 + }, + { + "id" : 7622, + "is_active" : null, + "name" : "4.11.97", + "sort_key" : 0 + }, + { + "id" : 7758, + "is_active" : null, + "name" : "4.12.0", + "sort_key" : 0 + }, + { + "id" : 8023, + "is_active" : null, + "name" : "4.12.1", + "sort_key" : 0 + }, + { + "id" : 8109, + "is_active" : null, + "name" : "4.12.2", + "sort_key" : 0 + }, + { + "id" : 8208, + "is_active" : null, + "name" : "4.12.3", + "sort_key" : 0 + }, + { + "id" : 8315, + "is_active" : null, + "name" : "4.12.4", + "sort_key" : 0 + }, + { + "id" : 8741, + "is_active" : null, + "name" : "4.12.5", + "sort_key" : 0 + }, + { + "id" : 7823, + "is_active" : null, + "name" : "4.12.60", + "sort_key" : 0 + }, + { + "id" : 8427, + "is_active" : null, + "name" : "4.12.80", + "sort_key" : 0 + }, + { + "id" : 8482, + "is_active" : null, + "name" : "4.12.90", + "sort_key" : 0 + }, + { + "id" : 8365, + "is_active" : null, + "name" : "4.12.95", + "sort_key" : 0 + }, + { + "id" : 8366, + "is_active" : null, + "name" : "4.12.97", + "sort_key" : 0 + }, + { + "id" : 8674, + "is_active" : null, + "name" : "4.13.0", + "sort_key" : 0 + }, + { + "id" : 8847, + "is_active" : null, + "name" : "4.13.1", + "sort_key" : 0 + }, + { + "id" : 8990, + "is_active" : null, + "name" : "4.13.2", + "sort_key" : 0 + }, + { + "id" : 9260, + "is_active" : null, + "name" : "4.13.3", + "sort_key" : 0 + }, + { + "id" : 9261, + "is_active" : null, + "name" : "4.14.0", + "sort_key" : 0 + }, + { + "id" : 9437, + "is_active" : null, + "name" : "4.14.1", + "sort_key" : 0 + }, + { + "id" : 9438, + "is_active" : null, + "name" : "4.14.2", + "sort_key" : 0 + }, + { + "id" : 9439, + "is_active" : null, + "name" : "4.14.3", + "sort_key" : 0 + }, + { + "id" : 8974, + "is_active" : null, + "name" : "4.60", + "sort_key" : 0 + }, + { + "id" : 10397, + "is_active" : null, + "name" : "14.12.3", + "sort_key" : 0 + }, + { + "id" : 10390, + "is_active" : null, + "name" : "15.04.0", + "sort_key" : 0 + }, + { + "id" : 10393, + "is_active" : null, + "name" : "15.04.3", + "sort_key" : 0 + }, + { + "id" : 10386, + "is_active" : null, + "name" : "15.08.0", + "sort_key" : 0 + }, + { + "id" : 10387, + "is_active" : null, + "name" : "15.08.1", + "sort_key" : 0 + }, + { + "id" : 10388, + "is_active" : null, + "name" : "15.08.2", + "sort_key" : 0 + }, + { + "id" : 10389, + "is_active" : null, + "name" : "15.08.3", + "sort_key" : 0 + }, + { + "id" : 10506, + "is_active" : null, + "name" : "15.12.0", + "sort_key" : 0 + }, + { + "id" : 10653, + "is_active" : null, + "name" : "15.12.1", + "sort_key" : 0 + }, + { + "id" : 11976, + "is_active" : null, + "name" : "15.12.3", + "sort_key" : 0 + }, + { + "id" : 13008, + "is_active" : null, + "name" : "16.08.0", + "sort_key" : 0 + }, + { + "id" : 13009, + "is_active" : null, + "name" : "16.08.1", + "sort_key" : 0 + }, + { + "id" : 13233, + "is_active" : null, + "name" : "16.08.2", + "sort_key" : 0 + }, + { + "id" : 13414, + "is_active" : null, + "name" : "16.08.3", + "sort_key" : 0 + }, + { + "id" : 13562, + "is_active" : null, + "name" : "16.12.0", + "sort_key" : 0 + }, + { + "id" : 14201, + "is_active" : null, + "name" : "16.12.1", + "sort_key" : 0 + }, + { + "id" : 3098, + "is_active" : null, + "name" : "16.12.2", + "sort_key" : 0 + }, + { + "id" : 14202, + "is_active" : null, + "name" : "16.12.3", + "sort_key" : 0 + }, + { + "id" : 14212, + "is_active" : null, + "name" : "17.03.80", + "sort_key" : 0 + }, + { + "id" : 14292, + "is_active" : null, + "name" : "17.04.0", + "sort_key" : 0 + }, + { + "id" : 14512, + "is_active" : null, + "name" : "17.04.1", + "sort_key" : 0 + }, + { + "id" : 14640, + "is_active" : null, + "name" : "17.04.2", + "sort_key" : 0 + }, + { + "id" : 14901, + "is_active" : null, + "name" : "17.04.3", + "sort_key" : 0 + }, + { + "id" : 14951, + "is_active" : null, + "name" : "17.07.80", + "sort_key" : 0 + }, + { + "id" : 15107, + "is_active" : null, + "name" : "17.08.0", + "sort_key" : 0 + }, + { + "id" : 15293, + "is_active" : null, + "name" : "17.08.1", + "sort_key" : 0 + }, + { + "id" : 15408, + "is_active" : null, + "name" : "17.08.2", + "sort_key" : 0 + }, + { + "id" : 15754, + "is_active" : null, + "name" : "17.08.3", + "sort_key" : 0 + }, + { + "id" : 15913, + "is_active" : null, + "name" : "17.12.0", + "sort_key" : 0 + }, + { + "id" : 15977, + "is_active" : null, + "name" : "17.12.1", + "sort_key" : 0 + }, + { + "id" : 16244, + "is_active" : null, + "name" : "17.12.2", + "sort_key" : 0 + }, + { + "id" : 16542, + "is_active" : null, + "name" : "17.12.3", + "sort_key" : 0 + }, + { + "id" : 16777, + "is_active" : null, + "name" : "18.04.0", + "sort_key" : 0 + }, + { + "id" : 16956, + "is_active" : null, + "name" : "18.04.1", + "sort_key" : 0 + }, + { + "id" : 17143, + "is_active" : null, + "name" : "18.04.2", + "sort_key" : 0 + }, + { + "id" : 17319, + "is_active" : null, + "name" : "18.04.3", + "sort_key" : 0 + }, + { + "id" : 17541, + "is_active" : null, + "name" : "18.08.0", + "sort_key" : 0 + }, + { + "id" : 17676, + "is_active" : null, + "name" : "18.08.1", + "sort_key" : 0 + }, + { + "id" : 17903, + "is_active" : null, + "name" : "18.08.2", + "sort_key" : 0 + }, + { + "id" : 18095, + "is_active" : null, + "name" : "18.08.3", + "sort_key" : 0 + }, + { + "id" : 18355, + "is_active" : null, + "name" : "18.12.0", + "sort_key" : 0 + }, + { + "id" : 18435, + "is_active" : null, + "name" : "18.12.1", + "sort_key" : 0 + }, + { + "id" : 18574, + "is_active" : null, + "name" : "18.12.2", + "sort_key" : 0 + }, + { + "id" : 18828, + "is_active" : null, + "name" : "18.12.3", + "sort_key" : 0 + }, + { + "id" : 19090, + "is_active" : null, + "name" : "19.04.0", + "sort_key" : 0 + }, + { + "id" : 19179, + "is_active" : null, + "name" : "19.04.1", + "sort_key" : 0 + }, + { + "id" : 19323, + "is_active" : null, + "name" : "19.04.2", + "sort_key" : 0 + }, + { + "id" : 14216, + "is_active" : null, + "name" : "unspecified", + "sort_key" : 0 + } + ] + } + ] +} + diff --git a/src/tests/integration/duplicate_attach_test.rb b/src/tests/integration/duplicate_attach_test.rb index daaff237..357fc990 100644 --- a/src/tests/integration/duplicate_attach_test.rb +++ b/src/tests/integration/duplicate_attach_test.rb @@ -1,228 +1,215 @@ # Copyright (C) 2017 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 # published by the Free Software Foundation; either version 2 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 14 of version 3 of the license. # # This program 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . require_relative 'test_helper' -require 'xmlrpc/client' -require 'xmlrpc/server' - -# Monkey patch the xmlrpc server to let us handle regular GET requests. -# Drkonqi partially goes through regular bugzilla cgi's simply requesting xml -# output. -module XMLServerInterceptor - # raw xml data is here - # def process(*args) - # warn "+++ #{__method__} +++" - # p args - # warn "--- #{__method__} ---" - # super - # end - - def service(req, resp) - # Where webrick comes in with request, server rejects non-xmlrpc requests - # so we'll manually handle GET requests as necessary and forward to an - # actual bugzilla so we don't have to reimplement everything. - warn "+++ #{__method__} +++" - if req.request_method == 'GET' - if req.request_uri.path.include?('buglist.cgi') # Returns CSV. - resp.body = <<-EOF -bug_id,"bug_severity","priority","bug_status","product","short_desc","resolution" -375161,"crash","NOR","NEEDSINFO","dolphin","Dolphin crash, copy from Samba share","BACKTRACE" - EOF - return - end - - if req.request_uri.path.include?('show_bug.cgi') - uri = req.request_uri.dup - uri.host = 'bugstest.kde.org' - uri.scheme = 'https' - uri.port = nil - resp.set_redirect(WEBrick::HTTPStatus::TemporaryRedirect, uri.to_s) - return - end - end - warn "--- #{__method__} ---" - super - end -end - -class XMLRPC::Server - prepend XMLServerInterceptor +require 'json' +require 'webrick' - # Expose webrick server so we can get our port :| - # https://github.com/ruby/xmlrpc/issues/17 - attr_accessor :server +# monkey patch alias from put to get, upstream only aliases post -.- +class WEBrick::HTTPServlet::ProcHandler + alias do_PUT do_GET end class TestDuplicateAttach < ATSPITest def setup - server = XMLRPC::Server.new(0) - port = server.server.config.fetch(:Port) + server = WEBrick::HTTPServer.new(Port: 0) + + port = server.config.fetch(:Port) ENV['DRKONQI_KDE_BUGZILLA_URL'] = "http://localhost:#{port}/" @got_comment = false - server.set_default_handler do |name, args| - puts '+++ handler +++' - p name, args - if name == 'User.login' - next {"id"=>12345, "token"=>"12345-cJ5o717AbC"} - end - if name == 'Bug.update' - id = args.fetch('ids').fetch(0) - cc_to_add = args.fetch('cc').fetch('add') - next {"bugs"=>[{"last_change_time"=>DateTime.now, "id"=>id, "changes"=>{"cc"=>{"removed"=>"", "added"=>cc_to_add}}, "alias"=>[]}]} - end - if name == 'Bug.add_attachment' - # Check for garbage string from test - @got_comment = args.fetch('comment').include?('yyyyyyyyyyyyyyyy') - next { "ids" => [1234] } + server.mount_proc '/' do |req, res| + query = req.request_uri.query + + case req.request_method + when 'GET' + case req.path_info + when '/rest/version' + res.body = JSON.generate(version: '5.0.6') + next + when '/rest/product/ruby' # this is off because the product detection is a bit meh + res.body = File.read("#{__dir__}/data/product.dolphin") + next + when '/rest/login' + unless query.include?('login=xxx') && query.include?('password=yyy') + abort + end + + res.body = JSON.generate(token: '123', id: '321') + next + when '/rest/bug', '/rest/bug/375161' + res.body = File.read("#{__dir__}/data/bugs") + next + when '/rest/bug/375161/comment' + res.body = File.read("#{__dir__}/data/comments") + next + end + when 'PUT' + case req.path_info + when '/rest/bug/375161' + input = JSON.parse(req.body) + if input['cc']&.[]('add')&.include?('xxx') + res.body = JSON.generate(id: 375161) + next + end + end + when 'POST' + case req.path_info + when '/rest/bug/375161/attachment' + input = JSON.parse(req.body) + if input['comment']&.include?('yyyyyyyyy') + res.body = JSON.generate(ids: [375161]) + @got_comment = true + next + end + end end - puts '~~~ bugzilla ~~~' - # Pipe request through bugstest. - # The arguments are killing me. - client = XMLRPC::Client.new('bugstest.kde.org', '/xmlrpc.cgi', 443, - nil, nil, nil, nil, true) - bugzilla = client.call(name, *args) - p bugzilla - next bugzilla + + warn "!!!!!!!!" + res.keep_alive = false + abort "ERROR Unexpected request #{req}" end - @xml_server_thread = Thread.start { server.serve } + Thread.report_on_exception = true + @api_server_thread = Thread.start { server.start } + @api_server_thread.report_on_exception=true @tracee = fork { loop { sleep(999_999_999) } } assert File.exist?(DRKONQI_PATH), "drkonqi not at #{DRKONQI_PATH}" @drkonqi_pid = spawn(DRKONQI_PATH, '--signal', '11', '--pid', @tracee.to_s, '--bugaddress', 'submit@bugs.kde.org', '--dialog') puts "pid: #{@drkonqi_pid}" end def teardown Process.kill('KILL', @tracee) Process.waitpid2(@tracee) - @xml_server_thread.kill - @xml_server_thread.join + @api_server_thread.kill + @api_server_thread.join end def drkonqi_running? Process.waitpid(@drkonqi_pid, Process::WNOHANG).nil? end # When evaluating duplicates def test_duplicate_attach drkonqi = nil 8.times do # be gracious for drkonqi to come up an atspi drkonqi = ATSPI.applications.find { |x| x.name == 'drkonqi' } break if drkonqi sleep 2 end refute_nil drkonqi, 'Could not find drkonqi on atspi api.' \ " It is running: #{drkonqi_running?}" accessible = find_in(drkonqi.windows[-1], name: 'Report Bug') press(accessible) find_in(drkonqi, name: 'Crash Reporting Assistant') do |window| accessible = find_in(window, name: 'Next') press(accessible) accessible = find_in(window, name: 'Yes') toggle_on(accessible) accessible = find_in(window, name: /^What I was doing when the application.+/) toggle_on(accessible) accessible = find_in(window, name: 'Next') press(accessible) loop do # Drkonqi is now doing the trace, wait until it is done. accessible = find_in(window, name: 'Next') refute_nil accessible if accessible.states.include?(:sensitive) press(accessible) break end warn accessible.states sleep 2 end # Set pseudo login data if there are none. accessible = find_in(window, name: 'Username input') accessible.text.set_to 'xxx' if accessible.text.length <= 0 - accessible = find_in(window, name: 'Password input') + # the lineedit is in fact an element on the input. why wouldn't it be... + accessible = find_in(window, name: 'Password input').children[0] accessible.text.set_to 'yyy' if accessible.text.length <= 0 accessible = find_in(window, name: 'Login') press(accessible) sleep 2 # Wait for login and bug listing accessible = find_in(window, name: '375161') toggle_on(accessible) accessible = find_in(window, name: 'Open selected report') press(accessible) end find_in(drkonqi, name: 'Bug Description') do |window| accessible = find_in(window, name: 'Suggest this crash is related') press(accessible) end find_in(drkonqi, name: 'Related Bug Report') do |window| accessible = find_in(window, name: /^Completely sure: attach my information.+/) toggle_on(accessible) accessible = find_in(window, name: 'Continue') press(accessible) end find_in(drkonqi, name: 'Crash Reporting Assistant') do |window| accessible = find_in(window, name: /^The report is going to be attached.+/) refute_nil accessible accessible = find_in(window, name: 'Next') press(accessible) accessible = find_in(window, name: 'Information about the crash text') accessible.text.set_to(accessible.text.to_s + Array.new(128).collect { 'y' }.join) accessible = find_in(window, name: 'Next') press(accessible) accessible = find_in(window, name: 'Next') press(accessible) accessible = find_in(window, name: /.*Crash report sent.*/) refute_nil accessible accessible = find_in(window, name: 'Finish') press(accessible) end assert @got_comment # only true iff the server go tour yyyyyy garbage string end end