From 4aef9ab81b739f655521133901f86b9ffff5cfc6 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Thu, 26 Mar 2020 10:50:45 +1000 Subject: [PATCH] [FEATURE] Inbuilt network logging tool This tool, which is available from the new F12 "dev tools" panel, is a native port of @rduivenvoorde's network logger plugin. It shows a list of ongoing and completed network requests, along with a whole load of useful detail like request and reply status, headers, errors, SSL configuration errors, timeouts, cache status, etc. Also has loads of polish and useful capabilities, such as the ability to filter requests by URL substrings and status, and you can right-click requests to open the URL in a browser or copy them as a cURL command. --- images/images.qrc | 1 + images/themes/default/mIconNetworkLogger.svg | 1 + src/app/CMakeLists.txt | 6 + .../networklogger/qgsnetworklogger.cpp | 328 +++++++++++ .../devtools/networklogger/qgsnetworklogger.h | 158 ++++++ .../networklogger/qgsnetworkloggernode.cpp | 517 ++++++++++++++++++ .../networklogger/qgsnetworkloggernode.h | 492 +++++++++++++++++ .../qgsnetworkloggerpanelwidget.cpp | 144 +++++ .../qgsnetworkloggerpanelwidget.h | 97 ++++ .../qgsnetworkloggerwidgetfactory.cpp | 29 + .../qgsnetworkloggerwidgetfactory.h | 35 ++ src/app/qgisapp.cpp | 14 +- src/app/qgisapp.h | 5 + src/ui/qgsnetworkloggerpanelbase.ui | 126 +++++ 14 files changed, 1952 insertions(+), 1 deletion(-) create mode 100644 images/themes/default/mIconNetworkLogger.svg create mode 100644 src/app/devtools/networklogger/qgsnetworklogger.cpp create mode 100644 src/app/devtools/networklogger/qgsnetworklogger.h create mode 100644 src/app/devtools/networklogger/qgsnetworkloggernode.cpp create mode 100644 src/app/devtools/networklogger/qgsnetworkloggernode.h create mode 100644 src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.cpp create mode 100644 src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.h create mode 100644 src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp create mode 100644 src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.h create mode 100644 src/ui/qgsnetworkloggerpanelbase.ui diff --git a/images/images.qrc b/images/images.qrc index 45f9503dbcf..0d3a640950c 100644 --- a/images/images.qrc +++ b/images/images.qrc @@ -834,6 +834,7 @@ themes/default/temporal_navigation/skipToEnd.svg themes/default/temporal_navigation/pause.svg themes/default/mIconIterate.svg + themes/default/mIconNetworkLogger.svg qgis_tips/symbol_levels.png diff --git a/images/themes/default/mIconNetworkLogger.svg b/images/themes/default/mIconNetworkLogger.svg new file mode 100644 index 00000000000..32d1e04035c --- /dev/null +++ b/images/themes/default/mIconNetworkLogger.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index 303795086b5..b6baec33d7e 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -164,6 +164,11 @@ SET(QGIS_APP_SRCS browser/qgsinbuiltdataitemproviders.cpp + devtools/networklogger/qgsnetworklogger.cpp + devtools/networklogger/qgsnetworkloggernode.cpp + devtools/networklogger/qgsnetworkloggerpanelwidget.cpp + devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp + labeling/qgslabelpropertydialog.cpp labeling/qgsmaptoolchangelabelproperties.cpp labeling/qgsmaptoolpinlabels.cpp @@ -373,6 +378,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/app ${CMAKE_SOURCE_DIR}/src/app/decorations + ${CMAKE_SOURCE_DIR}/src/app/devtools/networklogger ${CMAKE_SOURCE_DIR}/src/app/labeling ${CMAKE_SOURCE_DIR}/src/app/layout ${CMAKE_SOURCE_DIR}/src/app/pluginmanager diff --git a/src/app/devtools/networklogger/qgsnetworklogger.cpp b/src/app/devtools/networklogger/qgsnetworklogger.cpp new file mode 100644 index 00000000000..14683de667d --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworklogger.cpp @@ -0,0 +1,328 @@ +/*************************************************************************** + qgsnetworklogger.cpp + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "qgsnetworklogger.h" +#include "qgsnetworkloggernode.h" +#include "qgssettings.h" +#include "qgis.h" +#include +#include +#include + +QgsNetworkLogger::QgsNetworkLogger( QgsNetworkAccessManager *manager, QObject *parent ) + : QAbstractItemModel( parent ) + , mNam( manager ) + , mRootNode( qgis::make_unique< QgsNetworkLoggerRootNode >() ) +{ + // logger must be created on the main thread + Q_ASSERT( QThread::currentThread() == QApplication::instance()->thread() ); + Q_ASSERT( mNam->thread() == QApplication::instance()->thread() ); + + if ( QgsSettings().value( QStringLiteral( "logNetworkRequests" ), false, QgsSettings::App ).toBool() ) + enableLogging( true ); +} + +bool QgsNetworkLogger::isLogging() const +{ + return mIsLogging; +} + +QgsNetworkLogger::~QgsNetworkLogger() = default; + +void QgsNetworkLogger::enableLogging( bool enabled ) +{ + if ( enabled ) + { + connect( mNam, qgis::overload< QgsNetworkRequestParameters >::of( &QgsNetworkAccessManager::requestAboutToBeCreated ), this, &QgsNetworkLogger::requestAboutToBeCreated, Qt::UniqueConnection ); + connect( mNam, qgis::overload< QgsNetworkReplyContent >::of( &QgsNetworkAccessManager::finished ), this, &QgsNetworkLogger::requestFinished, Qt::UniqueConnection ); + connect( mNam, qgis::overload< QgsNetworkRequestParameters >::of( &QgsNetworkAccessManager::requestTimedOut ), this, &QgsNetworkLogger::requestTimedOut, Qt::UniqueConnection ); + connect( mNam, &QgsNetworkAccessManager::downloadProgress, this, &QgsNetworkLogger::downloadProgress, Qt::UniqueConnection ); + connect( mNam, &QgsNetworkAccessManager::requestEncounteredSslErrors, this, &QgsNetworkLogger::requestEncounteredSslErrors, Qt::UniqueConnection ); + } + else + { + disconnect( mNam, qgis::overload< QgsNetworkRequestParameters >::of( &QgsNetworkAccessManager::requestAboutToBeCreated ), this, &QgsNetworkLogger::requestAboutToBeCreated ); + disconnect( mNam, qgis::overload< QgsNetworkReplyContent >::of( &QgsNetworkAccessManager::finished ), this, &QgsNetworkLogger::requestFinished ); + disconnect( mNam, qgis::overload< QgsNetworkRequestParameters >::of( &QgsNetworkAccessManager::requestTimedOut ), this, &QgsNetworkLogger::requestTimedOut ); + disconnect( mNam, &QgsNetworkAccessManager::downloadProgress, this, &QgsNetworkLogger::downloadProgress ); + disconnect( mNam, &QgsNetworkAccessManager::requestEncounteredSslErrors, this, &QgsNetworkLogger::requestEncounteredSslErrors ); + } + mIsLogging = enabled; +} + +void QgsNetworkLogger::clear() +{ + beginResetModel(); + mRequestGroups.clear(); + mRootNode->clear(); + endResetModel(); +} + +void QgsNetworkLogger::requestAboutToBeCreated( QgsNetworkRequestParameters parameters ) +{ + const int childCount = mRootNode->childCount(); + + beginInsertRows( QModelIndex(), childCount, childCount ); + + std::unique_ptr< QgsNetworkLoggerRequestGroup > group = qgis::make_unique< QgsNetworkLoggerRequestGroup >( parameters ); + mRequestGroups.insert( parameters.requestId(), group.get() ); + mRootNode->addChild( std::move( group ) ); + endInsertRows(); + + if ( childCount > ( MAX_LOGGED_REQUESTS * 1.2 ) ) // 20 % more as buffer + trimRequests( childCount - MAX_LOGGED_REQUESTS ); +} + +void QgsNetworkLogger::requestFinished( QgsNetworkReplyContent content ) +{ + QgsNetworkLoggerRequestGroup *requestGroup = mRequestGroups.value( content.requestId() ); + if ( !requestGroup ) + return; + + // find the row: the position of the request in the rootNode + const QModelIndex requestIndex = node2index( requestGroup ); + if ( !requestIndex.isValid() ) + return; + + beginInsertRows( requestIndex, requestGroup->childCount(), requestGroup->childCount() ); + requestGroup->setReply( content ); + endInsertRows(); + + emit dataChanged( requestIndex, requestIndex ); +} + +void QgsNetworkLogger::requestTimedOut( QgsNetworkRequestParameters parameters ) +{ + QgsNetworkLoggerRequestGroup *requestGroup = mRequestGroups.value( parameters.requestId() ); + if ( !requestGroup ) + return; + + const QModelIndex requestIndex = node2index( requestGroup ); + if ( !requestIndex.isValid() ) + return; + + requestGroup->setTimedOut(); + + emit dataChanged( requestIndex, requestIndex ); +} + +void QgsNetworkLogger::downloadProgress( int requestId, qint64 bytesReceived, qint64 bytesTotal ) +{ + QgsNetworkLoggerRequestGroup *requestGroup = mRequestGroups.value( requestId ); + if ( !requestGroup ) + return; + + const QModelIndex requestIndex = node2index( requestGroup ); + if ( !requestIndex.isValid() ) + return; + + requestGroup->setProgress( bytesReceived, bytesTotal ); + + emit dataChanged( requestIndex, requestIndex, QVector() << Qt::ToolTipRole ); +} + +void QgsNetworkLogger::requestEncounteredSslErrors( int requestId, const QList &errors ) +{ + QgsNetworkLoggerRequestGroup *requestGroup = mRequestGroups.value( requestId ); + if ( !requestGroup ) + return; + + const QModelIndex requestIndex = node2index( requestGroup ); + if ( !requestIndex.isValid() ) + return; + + beginInsertRows( requestIndex, requestGroup->childCount(), requestGroup->childCount() ); + requestGroup->setSslErrors( errors ); + endInsertRows(); + + emit dataChanged( requestIndex, requestIndex ); +} + +QgsNetworkLoggerNode *QgsNetworkLogger::index2node( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + return mRootNode.get(); + + return reinterpret_cast( index.internalPointer() ); +} + +QList QgsNetworkLogger::actions( const QModelIndex &index, QObject *parent ) +{ + QgsNetworkLoggerNode *node = index2node( index ); + if ( !node ) + return QList< QAction * >(); + + return node->actions( parent ); +} + +QModelIndex QgsNetworkLogger::node2index( QgsNetworkLoggerNode *node ) const +{ + if ( !node || !node->parent() ) + return QModelIndex(); // this is the only root item -> invalid index + + QModelIndex parentIndex = node2index( node->parent() ); + + int row = node->parent()->indexOf( node ); + Q_ASSERT( row >= 0 ); + return index( row, 0, parentIndex ); +} + +QModelIndex QgsNetworkLogger::indexOfParentLayerTreeNode( QgsNetworkLoggerNode *parentNode ) const +{ + Q_ASSERT( parentNode ); + + QgsNetworkLoggerGroup *grandParentNode = parentNode->parent(); + if ( !grandParentNode ) + return QModelIndex(); // root node -> invalid index + + int row = grandParentNode->indexOf( parentNode ); + Q_ASSERT( row >= 0 ); + + return createIndex( row, 0, parentNode ); +} + +void QgsNetworkLogger::trimRequests( int count ) +{ + for ( int i = 0; i < count; ++i ) + { + int popId = data( index( i, 0, QModelIndex() ), QgsNetworkLoggerNode::RoleId ).toInt(); + mRequestGroups.remove( popId ); + } + + beginRemoveRows( QModelIndex(), 0, count - 1 ); + mRootNode->trimRequests( count ); + endRemoveRows(); +} + +int QgsNetworkLogger::rowCount( const QModelIndex &parent ) const +{ + QgsNetworkLoggerNode *n = index2node( parent ); + if ( !n ) + return 0; + + return n->childCount(); +} + +int QgsNetworkLogger::columnCount( const QModelIndex &parent ) const +{ + Q_UNUSED( parent ) + return 1; +} + +QModelIndex QgsNetworkLogger::index( int row, int column, const QModelIndex &parent ) const +{ + if ( column < 0 || column >= columnCount( parent ) || + row < 0 || row >= rowCount( parent ) ) + return QModelIndex(); + + QgsNetworkLoggerGroup *n = dynamic_cast< QgsNetworkLoggerGroup * >( index2node( parent ) ); + if ( !n ) + return QModelIndex(); // have no children + + return createIndex( row, column, n->childAt( row ) ); +} + +QModelIndex QgsNetworkLogger::parent( const QModelIndex &child ) const +{ + if ( !child.isValid() ) + return QModelIndex(); + + if ( QgsNetworkLoggerNode *n = index2node( child ) ) + { + return indexOfParentLayerTreeNode( n->parent() ); // must not be null + } + else + { + Q_ASSERT( false ); + return QModelIndex(); + } +} + +QVariant QgsNetworkLogger::data( const QModelIndex &index, int role ) const +{ + if ( !index.isValid() || index.column() > 1 ) + return QVariant(); + + QgsNetworkLoggerNode *node = index2node( index ); + if ( !node ) + return QVariant(); + + return node->data( role ); +} + +Qt::ItemFlags QgsNetworkLogger::flags( const QModelIndex &index ) const +{ + if ( !index.isValid() ) + { + Qt::ItemFlags rootFlags = Qt::ItemFlags(); + return rootFlags; + } + + Qt::ItemFlags f = Qt::ItemIsEnabled | Qt::ItemIsSelectable; + return f; +} + +QVariant QgsNetworkLogger::headerData( int section, Qt::Orientation orientation, int role ) const +{ + if ( section == 0 && orientation == Qt::Horizontal && role == Qt::DisplayRole ) + return tr( "Requests" ); + return QVariant(); +} + + +// +// QgsNetworkLoggerProxyModel +// + +QgsNetworkLoggerProxyModel::QgsNetworkLoggerProxyModel( QgsNetworkLogger *logger, QObject *parent ) + : QSortFilterProxyModel( parent ) + , mLogger( logger ) +{ + setSourceModel( mLogger ); +} + +void QgsNetworkLoggerProxyModel::setFilterString( const QString &string ) +{ + mFilterString = string; + invalidateFilter(); +} + +void QgsNetworkLoggerProxyModel::setShowSuccessful( bool show ) +{ + mShowSuccessful = show; + invalidateFilter(); +} + +void QgsNetworkLoggerProxyModel::setShowTimeouts( bool show ) +{ + mShowTimeouts = show; + invalidateFilter(); +} + +bool QgsNetworkLoggerProxyModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const +{ + QgsNetworkLoggerNode *node = mLogger->index2node( mLogger->index( source_row, 0, source_parent ) ); + if ( QgsNetworkLoggerRequestGroup *request = dynamic_cast< QgsNetworkLoggerRequestGroup * >( node ) ) + { + if ( ( request->status() == QgsNetworkLoggerRequestGroup::Status::Complete || request->status() == QgsNetworkLoggerRequestGroup::Status::Canceled ) + & !mShowSuccessful ) + return false; + else if ( request->status() == QgsNetworkLoggerRequestGroup::Status::TimeOut && !mShowTimeouts ) + return false; + return mFilterString.isEmpty() || request->url().url().contains( mFilterString, Qt::CaseInsensitive ); + } + + return true; +} diff --git a/src/app/devtools/networklogger/qgsnetworklogger.h b/src/app/devtools/networklogger/qgsnetworklogger.h new file mode 100644 index 00000000000..b5d3bfdfa7a --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworklogger.h @@ -0,0 +1,158 @@ +/*************************************************************************** + qgsnetworklogger.h + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +#ifndef QGSNETWORKLOGGER_H +#define QGSNETWORKLOGGER_H + +#include +#include +#include +#include "qgsnetworkaccessmanager.h" + +class QgsNetworkLoggerNode; +class QgsNetworkLoggerRequestGroup; +class QgsNetworkLoggerRootNode; +class QAction; + +/** + * \ingroup app + * \class QgsNetworkLogger + * \brief Logs network requests from a QgsNetworkAccessManager, converting them + * to a QAbstractItemModel representing the request and response details. + * + * \since QGIS 3.14 + */ +class QgsNetworkLogger : public QAbstractItemModel +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsNetworkLogger, logging requests from the specified \a manager. + * + * \warning QgsNetworkLogger must be created on the main thread, using the main thread's + * QgsNetworkAccessManager instance. + */ + QgsNetworkLogger( QgsNetworkAccessManager *manager, QObject *parent ); + ~QgsNetworkLogger() override; + + /** + * Returns TRUE if the logger is currently logging activity. + */ + bool isLogging() const; + + // Implementation of virtual functions from QAbstractItemModel + + int rowCount( const QModelIndex &parent = QModelIndex() ) const override; + int columnCount( const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex index( int row, int column, const QModelIndex &parent = QModelIndex() ) const override; + QModelIndex parent( const QModelIndex &child ) const override; + QVariant data( const QModelIndex &index, int role = Qt::DisplayRole ) const override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QVariant headerData( int section, Qt::Orientation orientation, int role = Qt::DisplayRole ) const override; + + /** + * Returns node for given index. Returns root node for invalid index. + */ + QgsNetworkLoggerNode *index2node( const QModelIndex &index ) const; + + /** + * Returns a list of actions corresponding to the item at the specified \a index. + * + * The actions should be parented to \a parent. + */ + QList< QAction * > actions( const QModelIndex &index, QObject *parent ); + + public slots: + + /** + * Enables or disables logging, depending on the value of \a enabled. + */ + void enableLogging( bool enabled ); + + /** + * Clears all logged entries. + */ + void clear(); + + private slots: + void requestAboutToBeCreated( QgsNetworkRequestParameters parameters ); + void requestFinished( QgsNetworkReplyContent content ); + void requestTimedOut( QgsNetworkRequestParameters parameters ); + void downloadProgress( int requestId, qint64 bytesReceived, qint64 bytesTotal ); + void requestEncounteredSslErrors( int requestId, const QList &errors ); + + private: + + //! Returns index for a given node + QModelIndex node2index( QgsNetworkLoggerNode *node ) const; + QModelIndex indexOfParentLayerTreeNode( QgsNetworkLoggerNode *parentNode ) const; + void trimRequests( int count ); + + QgsNetworkAccessManager *mNam = nullptr; + bool mIsLogging = false; + + std::unique_ptr< QgsNetworkLoggerRootNode > mRootNode; + + QHash< int, QgsNetworkLoggerRequestGroup * > mRequestGroups; + + static constexpr int MAX_LOGGED_REQUESTS = 1000; +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerProxyModel + * \brief A proxy model for filtering QgsNetworkLogger models by url string subsets + * or request status. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerProxyModel : public QSortFilterProxyModel +{ + public: + + /** + * Constructor for QgsNetworkLoggerProxyModel, filtering the specified network \a logger. + */ + QgsNetworkLoggerProxyModel( QgsNetworkLogger *logger, QObject *parent ); + + /** + * Sets a filter \a string to apply to request URLs. + */ + void setFilterString( const QString &string ); + + /** + * Sets whether successful requests should be shown. + */ + void setShowSuccessful( bool show ); + + /** + * Sets whether timed out requests should be shown. + */ + void setShowTimeouts( bool show ); + + protected: + bool filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const override; + + private: + + QgsNetworkLogger *mLogger = nullptr; + + QString mFilterString; + bool mShowSuccessful = true; + bool mShowTimeouts = true; +}; + +#endif // QGSNETWORKLOGGER_H diff --git a/src/app/devtools/networklogger/qgsnetworkloggernode.cpp b/src/app/devtools/networklogger/qgsnetworkloggernode.cpp new file mode 100644 index 00000000000..6405ce7c606 --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggernode.cpp @@ -0,0 +1,517 @@ +/*************************************************************************** + qgsnetworkloggernode.cpp + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "qgsnetworkloggernode.h" +#include "qgis.h" +#include +#include +#include +#include +#include +#include +#include +#include +// +// QgsNetworkLoggerNode +// + +QgsNetworkLoggerNode::QgsNetworkLoggerNode() = default; +QgsNetworkLoggerNode::~QgsNetworkLoggerNode() = default; + + +// +// QgsNetworkLoggerGroup +// + +QgsNetworkLoggerGroup::QgsNetworkLoggerGroup( const QString &title ) + : mGroupTitle( title ) +{ +} + +void QgsNetworkLoggerGroup::addChild( std::unique_ptr child ) +{ + if ( !child ) + return; + + Q_ASSERT( !child->mParent ); + child->mParent = this; + + mChildren.emplace_back( std::move( child ) ); +} + +int QgsNetworkLoggerGroup::indexOf( QgsNetworkLoggerNode *child ) const +{ + Q_ASSERT( child->mParent == this ); + auto it = std::find_if( mChildren.begin(), mChildren.end(), [&]( const std::unique_ptr &p ) + { + return p.get() == child; + } ); + if ( it != mChildren.end() ) + return std::distance( mChildren.begin(), it ); + return -1; +} + +QgsNetworkLoggerNode *QgsNetworkLoggerGroup::childAt( int index ) +{ + Q_ASSERT( static_cast< std::size_t >( index ) < mChildren.size() ); + return mChildren[ index ].get(); +} + +void QgsNetworkLoggerGroup::clear() +{ + mChildren.clear(); +} + +QVariant QgsNetworkLoggerGroup::data( int role ) const +{ + switch ( role ) + { + case Qt::DisplayRole: + return mGroupTitle; + + default: + break; + } + return QVariant(); +} + +// +// QgsNetworkLoggerRootNode +// + +QgsNetworkLoggerRootNode::QgsNetworkLoggerRootNode() + : QgsNetworkLoggerGroup( QString() ) +{ + +} + +QVariant QgsNetworkLoggerRootNode::data( int ) const +{ + return QVariant(); +} + +void QgsNetworkLoggerRootNode::trimRequests( int count ) +{ + for ( int i = 0; i < count; ++i ) + mChildren.pop_front(); +} + + +// +// QgsNetworkLoggerValueNode +// +QgsNetworkLoggerValueNode::QgsNetworkLoggerValueNode( const QString &key, const QString &value, const QColor &color ) + : mKey( key ) + , mValue( value ) + , mColor( color ) +{ + +} + +QVariant QgsNetworkLoggerValueNode::data( int role ) const +{ + switch ( role ) + { + case Qt::DisplayRole: + case Qt::ToolTipRole: + { + return QStringLiteral( "%1: %2" ).arg( mKey.leftJustified( 30, ' ' ), mValue ); + } + + case Qt::ForegroundRole: + { + if ( mColor.isValid() ) + return QBrush( mColor ); + break; + } + default: + break; + } + return QVariant(); +} + +// +// QgsNetworkLoggerGroup +// + +void QgsNetworkLoggerGroup::addKeyValueNode( const QString &key, const QString &value, const QColor &color ) +{ + addChild( qgis::make_unique< QgsNetworkLoggerValueNode >( key, value, color ) ); +} + + +// +// QgsNetworkLoggerRequestGroup +// + +QgsNetworkLoggerRequestGroup::QgsNetworkLoggerRequestGroup( const QgsNetworkRequestParameters &request ) + : QgsNetworkLoggerGroup( QString() ) + , mUrl( request.request().url() ) + , mRequestId( request.requestId() ) + , mOperation( request.operation() ) + , mData( request.content() ) +{ + const QList headers = request.request().rawHeaderList(); + for ( const QByteArray &header : headers ) + { + mHeaders.append( qMakePair( QString( header ), QString( request.request().rawHeader( header ) ) ) ); + } + + std::unique_ptr< QgsNetworkLoggerRequestDetailsGroup > detailsGroup = qgis::make_unique< QgsNetworkLoggerRequestDetailsGroup >( request ); + addChild( std::move( detailsGroup ) ); + + mTimer.start(); +} + +QVariant QgsNetworkLoggerRequestGroup::data( int role ) const +{ + switch ( role ) + { + case Qt::DisplayRole: + return QStringLiteral( "%1 %2 %3" ).arg( QString::number( mRequestId ), + operationToString( mOperation ), + mUrl.url() ); + + case Qt::ToolTipRole: + { + QString bytes = QObject::tr( "unknown" ); + if ( mBytesTotal != 0 ) + { + if ( mBytesReceived > 0 && mBytesReceived < mBytesTotal ) + bytes = QStringLiteral( "%1/%2" ).arg( QString::number( mBytesReceived ), QString::number( mBytesTotal ) ); + else if ( mBytesReceived > 0 && mBytesReceived == mBytesTotal ) + bytes = QString::number( mBytesTotal ); + } + // ?? adding
instead of \n after (very long) url seems to break url up + // COMPLETE, Status: 200 - text/xml; charset=utf-8 - 2334 bytes - 657 milliseconds + return QStringLiteral( "%1
%2 - Status: %3 - %4 - %5 bytes - %6 msec - %7 replies" ) + .arg( mUrl.url(), + statusToString( mStatus ), + QString::number( mHttpStatus ), + mContentType, + bytes, + mStatus == Status::Pending ? QString::number( mTimer.elapsed() / 1000 ) : QString::number( mTotalTime ), + QString::number( mReplies ) ); + } + + case RoleStatus: + return static_cast< int >( mStatus ); + + case RoleId: + return mRequestId; + + case Qt::ForegroundRole: + { + if ( mHasSslErrors ) + return QBrush( QColor( 180, 65, 210 ) ); + switch ( mStatus ) + { + case QgsNetworkLoggerRequestGroup::Status::Pending: + case QgsNetworkLoggerRequestGroup::Status::Canceled: + return QBrush( QColor( 0, 0, 0, 100 ) ); + case QgsNetworkLoggerRequestGroup::Status::Error: + return QBrush( QColor( 235, 10, 10 ) ); + case QgsNetworkLoggerRequestGroup::Status::TimeOut: + return QBrush( QColor( 235, 10, 10 ) ); + case QgsNetworkLoggerRequestGroup::Status::Complete: + break; + } + break; + } + + case Qt::FontRole: + { + if ( mStatus == Status::Canceled ) + { + QFont f; + f.setStrikeOut( true ); + return f; + } + break; + } + + default: + break; + } + return QVariant(); +} + +QList QgsNetworkLoggerRequestGroup::actions( QObject *parent ) +{ + QList< QAction * > res; + QAction *openUrlAction = new QAction( QObject::tr( "Open URL" ), parent ); + QObject::connect( openUrlAction, &QAction::triggered, openUrlAction, [ = ] + { + QDesktopServices::openUrl( mUrl ); + } ); + res << openUrlAction; + + QAction *copyAsCurlAction = new QAction( QObject::tr( "Copy As cURL" ), parent ); + QObject::connect( copyAsCurlAction, &QAction::triggered, copyAsCurlAction, [ = ] + { + QString curlHeaders; + for ( const QPair< QString, QString > &header : qgis::as_const( mHeaders ) ) + curlHeaders += QStringLiteral( "-H '%1: %2' " ).arg( header.first, header.second ); + + QString curlData; + if ( mOperation == QNetworkAccessManager::PostOperation || mOperation == QNetworkAccessManager::PutOperation ) + curlData = QStringLiteral( "--data '%1' " ).arg( QString( mData ) ); + + QString curlCmd = QStringLiteral( "curl '%1' %2 %3--compressed" ).arg( + mUrl.url(), + curlHeaders, + curlData ); + QApplication::clipboard()->setText( curlCmd ); + } ); + res << copyAsCurlAction; + + return res; +} + +void QgsNetworkLoggerRequestGroup::setReply( const QgsNetworkReplyContent &reply ) +{ + switch ( reply.error() ) + { + case QNetworkReply::OperationCanceledError: + mStatus = Status::Canceled; + break; + + case QNetworkReply::NoError: + mStatus = Status::Complete; + break; + + default: + mStatus = Status::Error; + break; + } + + mTotalTime = mTimer.elapsed(); + mHttpStatus = reply.attribute( QNetworkRequest::HttpStatusCodeAttribute ).toInt(); + mContentType = reply.rawHeader( "Content - Type" ); + + addChild( qgis::make_unique< QgsNetworkLoggerReplyGroup >( reply ) ); +} + +void QgsNetworkLoggerRequestGroup::setTimedOut() +{ + mStatus = Status::TimeOut; +} + +void QgsNetworkLoggerRequestGroup::setProgress( qint64 bytesReceived, qint64 bytesTotal ) +{ + mReplies++; + mBytesReceived = bytesReceived; + mBytesTotal = bytesTotal; +} + +void QgsNetworkLoggerRequestGroup::setSslErrors( const QList &errors ) +{ + mHasSslErrors = !errors.empty(); + if ( mHasSslErrors ) + { + addChild( qgis::make_unique< QgsNetworkLoggerSslErrorGroup >( errors ) ); + } +} + +QString QgsNetworkLoggerRequestGroup::operationToString( QNetworkAccessManager::Operation operation ) +{ + switch ( operation ) + { + case QNetworkAccessManager::HeadOperation: + return QStringLiteral( "HEAD" ); + case QNetworkAccessManager::GetOperation: + return QStringLiteral( "GET" ); + case QNetworkAccessManager::PutOperation: + return QStringLiteral( "PUT" ); + case QNetworkAccessManager::PostOperation: + return QStringLiteral( "POST" ); + case QNetworkAccessManager::DeleteOperation: + return QStringLiteral( "DELETE" ); + case QNetworkAccessManager::UnknownOperation: + return QStringLiteral( "UNKNOWN" ); + case QNetworkAccessManager::CustomOperation: + return QStringLiteral( "CUSTOM" ); + } + return QString(); +} + +QString QgsNetworkLoggerRequestGroup::statusToString( QgsNetworkLoggerRequestGroup::Status status ) +{ + switch ( status ) + { + case QgsNetworkLoggerRequestGroup::Status::Pending: + return QObject::tr( "Pending" ); + case QgsNetworkLoggerRequestGroup::Status::Complete: + return QObject::tr( "Complete" ); + case QgsNetworkLoggerRequestGroup::Status::Error: + return QObject::tr( "Error" ); + case QgsNetworkLoggerRequestGroup::Status::TimeOut: + return QObject::tr( "Timeout" ); + case QgsNetworkLoggerRequestGroup::Status::Canceled: + return QObject::tr( "Canceled" ); + } + return QString(); +} + +QString QgsNetworkLoggerRequestGroup::cacheControlToString( QNetworkRequest::CacheLoadControl control ) +{ + switch ( control ) + { + case QNetworkRequest::AlwaysNetwork: + return QObject::tr( "Always load from network, do not check cache" ); + case QNetworkRequest::PreferNetwork: + return QObject::tr( "Load from the network if the cached entry is older than the network entry" ); + case QNetworkRequest::PreferCache: + return QObject::tr( "Load from cache if available, otherwise load from network" ); + case QNetworkRequest::AlwaysCache: + return QObject::tr( "Only load from cache, error if no cached entry available" ); + } + return QString(); +} + + +// +// QgsNetworkLoggerRequestDetailsGroup +// + +QgsNetworkLoggerRequestDetailsGroup::QgsNetworkLoggerRequestDetailsGroup( const QgsNetworkRequestParameters &request ) + : QgsNetworkLoggerGroup( QObject::tr( "Request" ) ) +{ + addKeyValueNode( QObject::tr( "Operation" ), QgsNetworkLoggerRequestGroup::operationToString( request.operation() ) ); + addKeyValueNode( QObject::tr( "Thread" ), request.originatingThreadId() ); + addKeyValueNode( QObject::tr( "Initiator" ), request.initiatorClassName().isEmpty() ? QObject::tr( "unknown" ) : request.initiatorClassName() ); + if ( request.initiatorRequestId().isValid() ) + addKeyValueNode( QObject::tr( "ID" ), request.initiatorRequestId().toString() ); + addKeyValueNode( QObject::tr( "Cache (control)" ), QgsNetworkLoggerRequestGroup::cacheControlToString( static_cast< QNetworkRequest::CacheLoadControl >( request.request().attribute( QNetworkRequest::CacheLoadControlAttribute ).toInt() ) ) ); + addKeyValueNode( QObject::tr( "Cache (save)" ), request.request().attribute( QNetworkRequest::CacheSaveControlAttribute ).toBool() ? QObject::tr( "Can store result in cache" ) : QObject::tr( "Result cannot be stored in cache" ) ); + + if ( !QUrlQuery( request.request().url() ).queryItems().isEmpty() ) + addChild( qgis::make_unique< QgsNetworkLoggerRequestQueryGroup >( request.request().url() ) ); + + addChild( qgis::make_unique< QgsNetworkLoggerRequestHeadersGroup >( request ) ); + + switch ( request.operation() ) + { + case QNetworkAccessManager::PostOperation: + case QNetworkAccessManager::PutOperation: + addChild( qgis::make_unique< QgsNetworkLoggerPostContentGroup >( request ) ); + break; + + case QNetworkAccessManager::GetOperation: + case QNetworkAccessManager::HeadOperation: + case QNetworkAccessManager::DeleteOperation: + case QNetworkAccessManager::UnknownOperation: + case QNetworkAccessManager::CustomOperation: + break; + } +} + +// +// QgsNetworkLoggerRequestQueryGroup +// + +QgsNetworkLoggerRequestQueryGroup::QgsNetworkLoggerRequestQueryGroup( const QUrl &url ) + : QgsNetworkLoggerGroup( QObject::tr( "Query" ) ) +{ + QUrlQuery query( url ); + const QList > queryItems = query.queryItems(); + + for ( const QPair< QString, QString > &query : queryItems ) + { + addKeyValueNode( query.first, query.second ); + } +} + + +// +// QgsNetworkLoggerRequestHeadersGroup +// +QgsNetworkLoggerRequestHeadersGroup::QgsNetworkLoggerRequestHeadersGroup( const QgsNetworkRequestParameters &request ) + : QgsNetworkLoggerGroup( QObject::tr( "Headers" ) ) +{ + const QList headers = request.request().rawHeaderList(); + for ( const QByteArray &header : headers ) + { + addKeyValueNode( header, request.request().rawHeader( header ) ); + } +} + +// +// QgsNetworkLoggerPostContentGroup +// + +QgsNetworkLoggerPostContentGroup::QgsNetworkLoggerPostContentGroup( const QgsNetworkRequestParameters ¶meters ) + : QgsNetworkLoggerGroup( QObject::tr( "Content" ) ) +{ + addKeyValueNode( QObject::tr( "Data" ), parameters.content() ); +} + + +// +// QgsNetworkLoggerReplyGroup +// + +QgsNetworkLoggerReplyGroup::QgsNetworkLoggerReplyGroup( const QgsNetworkReplyContent &reply ) + : QgsNetworkLoggerGroup( QObject::tr( "Reply" ) ) +{ + addKeyValueNode( QObject::tr( "Status" ), reply.attribute( QNetworkRequest::HttpStatusCodeAttribute ).toString() ); + if ( reply.error() != QNetworkReply::NoError ) + { + addKeyValueNode( QObject::tr( "Error Code" ), QString::number( static_cast< int >( reply.error() ) ) ); + addKeyValueNode( QObject::tr( "Error" ), reply.errorString() ); + } + addKeyValueNode( QObject::tr( "Cache (result)" ), reply.attribute( QNetworkRequest::SourceIsFromCacheAttribute ).toBool() ? QObject::tr( "Used entry from cache" ) : QObject::tr( "Read from network" ) ); + + addChild( qgis::make_unique< QgsNetworkLoggerReplyHeadersGroup >( reply ) ); +} + + +// +// QgsNetworkLoggerReplyHeadersGroup +// +QgsNetworkLoggerReplyHeadersGroup::QgsNetworkLoggerReplyHeadersGroup( const QgsNetworkReplyContent &reply ) + : QgsNetworkLoggerGroup( QObject::tr( "Headers" ) ) +{ + const QList headers = reply.rawHeaderList(); + for ( const QByteArray &header : headers ) + { + addKeyValueNode( header, reply.rawHeader( header ) ); + } +} + +// +// QgsNetworkLoggerSslErrorGroup +// +QgsNetworkLoggerSslErrorGroup::QgsNetworkLoggerSslErrorGroup( const QList &errors ) + : QgsNetworkLoggerGroup( QObject::tr( "SSL errors" ) ) +{ + for ( const QSslError &error : errors ) + { + addKeyValueNode( QObject::tr( "Error" ), error.errorString(), QColor( 180, 65, 210 ) ); + } +} + +QVariant QgsNetworkLoggerSslErrorGroup::data( int role ) const +{ + if ( role == Qt::ForegroundRole ) + return QBrush( QColor( 180, 65, 210 ) ); + + return QgsNetworkLoggerGroup::data( role ); +} + +QList QgsNetworkLoggerNode::actions( QObject * ) +{ + return QList< QAction * >(); +} diff --git a/src/app/devtools/networklogger/qgsnetworkloggernode.h b/src/app/devtools/networklogger/qgsnetworkloggernode.h new file mode 100644 index 00000000000..c4fcaa7afba --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggernode.h @@ -0,0 +1,492 @@ +/*************************************************************************** + qgsnetworkloggernode.h + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +#ifndef QGSNETWORKLOGGERNODE_H +#define QGSNETWORKLOGGERNODE_H + +#include "qgsnetworkaccessmanager.h" +#include +#include +#include +#include +#include + +class QAction; +class QgsNetworkLoggerGroup; + +/** + * \ingroup app + * \class QgsNetworkLoggerNode + * \brief Base class for nodes in the network logger model. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerNode +{ + public: + + //! Custom node data roles + enum Roles + { + RoleStatus = Qt::UserRole + 1, //!< Request status role + RoleId, //!< Request ID role + }; + + virtual ~QgsNetworkLoggerNode(); + + /** + * Returns the node's parent node. + * + * If parent is NULLPTR, the node is a root node + */ + QgsNetworkLoggerGroup *parent() { return mParent; } + + /** + * Returns the node's data for the specified model \a role. + */ + virtual QVariant data( int role = Qt::DisplayRole ) const = 0; + + /** + * Returns the number of child nodes owned by this node. + */ + virtual int childCount() const = 0; + + /** + * Returns a list of actions relating to the node. + * + * The actions should be parented to \a parent. + */ + virtual QList< QAction * > actions( QObject *parent ); + + protected: + + QgsNetworkLoggerNode(); + + private: + + QgsNetworkLoggerGroup *mParent = nullptr; + friend class QgsNetworkLoggerGroup; +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerGroup + * \brief Base class for network logger model "group" nodes, which contain children of their own. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerGroup : public QgsNetworkLoggerNode +{ + public: + + /** + * Adds a \a child node to this node. + */ + void addChild( std::unique_ptr< QgsNetworkLoggerNode > child ); + + /** + * Returns the index of the specified \a child node. + * + * \warning \a child must be a valid child of this node. + */ + int indexOf( QgsNetworkLoggerNode *child ) const; + + /** + * Returns the child at the specified \a index. + */ + QgsNetworkLoggerNode *childAt( int index ); + + /** + * Clears the group, removing all its children. + */ + void clear(); + + int childCount() const override final { return mChildren.size(); } + QVariant data( int role = Qt::DisplayRole ) const override; + + protected: + + /** + * Constructor for a QgsNetworkLoggerGroup, with the specified \a title. + */ + QgsNetworkLoggerGroup( const QString &title ); + + /** + * Adds a simple \a key: \a value node to the group. + */ + void addKeyValueNode( const QString &key, const QString &value, const QColor &color = QColor() ); + + private: + + std::deque< std::unique_ptr< QgsNetworkLoggerNode > > mChildren; + QString mGroupTitle; + friend class QgsNetworkLoggerRootNode; + +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerValueNode + * \brief A "key: value" style node for the network logger model. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerValueNode : public QgsNetworkLoggerNode +{ + public: + + /** + * Constructor for QgsNetworkLoggerValueNode, with the specified \a key (usually translated) and \a value. + */ + QgsNetworkLoggerValueNode( const QString &key, const QString &value, const QColor &color = QColor() ); + + QVariant data( int role = Qt::DisplayRole ) const override final; + int childCount() const override final { return 0; } + + private: + + QString mKey; + QString mValue; + QColor mColor; +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerRootNode + * \brief Root node for the network logger model. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerRootNode final : public QgsNetworkLoggerGroup +{ + public: + + QgsNetworkLoggerRootNode(); + QVariant data( int role = Qt::DisplayRole ) const override final; + + /** + * Removes \a count requests from the start of the root group. + */ + void trimRequests( int count ); +}; + + +/** + * \ingroup app + * \class QgsNetworkLoggerRequestGroup + * \brief Parent group for all network requests, showing the request id, type, url, + * and containing child groups with detailed request and response information. + * + * Visually, a QgsNetworkLoggerRequestGroup is structured by: + * + * |__ QgsNetworkLoggerRequestGroup (showing id, type (GET etc) url) + * |__ QgsNetworkLoggerRequestDetailsGroup (holding Request details) + * |__ QgsNetworkLoggerValueNode (key-value pairs with info) + * ... + * |__ QgsNetworkLoggerRequestQueryGroup (holding query info) + * |__ ... + * |__ QgsNetworkLoggerRequestHeadersGroup ('Headers') + * |__ ... + * |__ QgsNetworkLoggerPostContentGroup (showing Data in case of POST) + * |__ ... + * |__ QgsNetworkLoggerReplyGroup (holding Reply details) + * |__ QgsNetworkLoggerReplyHeadersGroup (Reply 'Headers') + * |__ ... + * |__ QgsNetworkLoggerSslErrorGroup (holding SSL error details, if encountered) + * |__ ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerRequestGroup final : public QgsNetworkLoggerGroup +{ + public: + + //! Request statu + enum class Status + { + Pending, //!< Request underway + Complete, //!< Request was successfully completed + Error, //!< Request encountered an error + TimeOut, //!< Request timed out + Canceled, //!< Request was manually canceled + }; + + /** + * Constructor for QgsNetworkLoggerRequestGroup, populated from the + * specified \a request details. + */ + QgsNetworkLoggerRequestGroup( const QgsNetworkRequestParameters &request ); + QVariant data( int role = Qt::DisplayRole ) const override; + QList< QAction * > actions( QObject *parent ) override final; + + /** + * Returns the request's status. + */ + Status status() const { return mStatus; } + + /** + * Returns the request's URL. + */ + QUrl url() const { return mUrl; } + + /** + * Called to set the \a reply associated with the request. + * + * Will automatically create children encapsulating the reply details. + */ + void setReply( const QgsNetworkReplyContent &reply ); + + /** + * Flags the reply as having timed out. + */ + void setTimedOut(); + + /** + * Sets the requests download progress. + */ + void setProgress( qint64 bytesReceived, qint64 bytesTotal ); + + /** + * Reports any SSL errors encountered while processing the request. + */ + void setSslErrors( const QList &errors ); + + /** + * Converts a network \a operation to a string value. + */ + static QString operationToString( QNetworkAccessManager::Operation operation ); + + /** + * Converts a request \a status to a translated string value. + */ + static QString statusToString( Status status ); + + /** + * Converts a cache load \a control value to a translated string. + */ + static QString cacheControlToString( QNetworkRequest::CacheLoadControl control ); + + private: + + QUrl mUrl; + int mRequestId = 0; + QNetworkAccessManager::Operation mOperation; + QElapsedTimer mTimer; + qint64 mTotalTime = 0; + int mHttpStatus = -1; + QString mContentType; + qint64 mBytesReceived = 0; + qint64 mBytesTotal = 0; + int mReplies = 0; + QByteArray mData; + Status mStatus = Status::Pending; + bool mHasSslErrors = false; + QList< QPair< QString, QString > > mHeaders; +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerRequestGroup + * \brief Parent group for all network request details, showing the request parameters + * and header information. + * + * Visually, a QgsNetworkLoggerRequestDetailsGroup is structured by: + * + * |__ QgsNetworkLoggerRequestDetailsGroup (holding Request details) + * |__ QgsNetworkLoggerValueNode (key-value pairs with info) + * Operation: ... + * Thread: ... + * Initiator: ... + * ID: ... + * Cache (control): ... + * Cache (save): ... + * |__ QgsNetworkLoggerRequestQueryGroup (holding query info) + * |__ ... + * |__ QgsNetworkLoggerRequestHeadersGroup ('Headers') + * |__ ... + * |__ QgsNetworkLoggerPostContentGroup (showing Data in case of POST) + * |__ ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerRequestDetailsGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerRequestDetailsGroup, populated from the + * specified \a request details. + */ + QgsNetworkLoggerRequestDetailsGroup( const QgsNetworkRequestParameters &request ); + +}; + + +/** + * \ingroup app + * \class QgsNetworkLoggerRequestHeadersGroup + * \brief Parent group for all network request header information. + * + * Visually, a QgsNetworkLoggerRequestHeadersGroup is structured by: + * + * |__ QgsNetworkLoggerRequestHeadersGroup (holding Request details) + * User-Agent: ... + * ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerRequestHeadersGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerRequestHeadersGroup, populated from the + * specified \a request details. + */ + QgsNetworkLoggerRequestHeadersGroup( const QgsNetworkRequestParameters &request ); + +}; + + +/** + * \ingroup app + * \class QgsNetworkLoggerRequestQueryGroup + * \brief Parent group for all network request query information. + * + * Visually, a QgsNetworkLoggerRequestQueryGroup is structured by: + * + * |__ QgsNetworkLoggerRequestQueryGroup (holding query info) + * query param: value + * ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerRequestQueryGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerRequestQueryGroup, populated from the + * specified \a url. + */ + QgsNetworkLoggerRequestQueryGroup( const QUrl &url ); + +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerPostContentGroup + * \brief Parent group for all request post data, showing POST data. + * + * Visually, a QgsNetworkLoggerPostContentGroup is structured by: + * + * |__ QgsNetworkLoggerPostContentGroup (holding POST data) + * |__ Data: POST data + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerPostContentGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerPostContentGroup, populated from the + * specified \a request details. + */ + QgsNetworkLoggerPostContentGroup( const QgsNetworkRequestParameters ¶meters ); +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerReplyGroup + * \brief Parent group for all network replies, showing the reply details. + * + * Visually, a QgsNetworkLoggerReplyGroup is structured by: + * + * |__ QgsNetworkLoggerReplyGroup (holding Reply details) + * |__ Status: reply status (e.g. 'Canceled') + * |__ Error code: code (if applicable) + * |__ Cache (result): whether reply was taken from cache or network + * |__ QgsNetworkLoggerReplyHeadersGroup (Reply 'Headers') + * |__ ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerReplyGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerReplyGroup, populated from the + * specified \a reply details. + */ + QgsNetworkLoggerReplyGroup( const QgsNetworkReplyContent &reply ); + +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerReplyHeadersGroup + * \brief Parent group for network reply headers, showing the reply header details. + * + * Visually, a QgsNetworkLoggerReplyHeadersGroup is structured by: + * + * |__ QgsNetworkLoggerReplyHeadersGroup (holding reply Header details) + * Content-Type: ... + * Content-Length: ... + * ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerReplyHeadersGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerReplyHeadersGroup, populated from the + * specified \a reply details. + */ + QgsNetworkLoggerReplyHeadersGroup( const QgsNetworkReplyContent &reply ); + +}; + +/** + * \ingroup app + * \class QgsNetworkLoggerSslErrorNode + * \brief Parent group for SSQL errors, showing the error details. + * + * Visually, a QgsNetworkLoggerSslErrorGroup is structured by: + * + * |__ QgsNetworkLoggerSslErrorGroup (holding error details) + * Error: ... + * Error: ... + * ... + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerSslErrorGroup final : public QgsNetworkLoggerGroup +{ + public: + + /** + * Constructor for QgsNetworkLoggerSslErrorGroup, populated from the + * specified \a errors. + */ + QgsNetworkLoggerSslErrorGroup( const QList &errors ); + QVariant data( int role = Qt::DisplayRole ) const override; +}; + + + +#endif // QGSNETWORKLOGGERNODE_H diff --git a/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.cpp b/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.cpp new file mode 100644 index 00000000000..f1d83cdace7 --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.cpp @@ -0,0 +1,144 @@ +/*************************************************************************** + qgsnetworkloggerpanelwidget.cpp + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "qgsnetworkloggerpanelwidget.h" +#include "qgsguiutils.h" +#include "qgsnetworklogger.h" +#include "qgssettings.h" +#include "qgsnetworkloggernode.h" +#include +#include + +// +// QgsNetworkLoggerTreeView +// + +QgsNetworkLoggerTreeView::QgsNetworkLoggerTreeView( QgsNetworkLogger *logger, QWidget *parent ) + : QTreeView( parent ) + , mLogger( logger ) +{ + connect( this, &QTreeView::expanded, this, &QgsNetworkLoggerTreeView::itemExpanded ); + + setFont( QFontDatabase::systemFont( QFontDatabase::FixedFont ) ); + + mProxyModel = new QgsNetworkLoggerProxyModel( mLogger, this ); + setModel( mProxyModel ); + + setContextMenuPolicy( Qt::CustomContextMenu ); + connect( this, &QgsNetworkLoggerTreeView::customContextMenuRequested, this, &QgsNetworkLoggerTreeView::contextMenu ); + + mMenu = new QMenu( this ); +} + +void QgsNetworkLoggerTreeView::setFilterString( const QString &string ) +{ + mProxyModel->setFilterString( string ); +} + +void QgsNetworkLoggerTreeView::setShowSuccessful( bool show ) +{ + mProxyModel->setShowSuccessful( show ); +} + +void QgsNetworkLoggerTreeView::setShowTimeouts( bool show ) +{ + mProxyModel->setShowTimeouts( show ); +} + +void QgsNetworkLoggerTreeView::itemExpanded( const QModelIndex &index ) +{ + // if the item is a QgsNetworkLoggerRequestGroup item, open all children (show ALL info of it) + // we want to scroll to last request + + // only expand all children on QgsNetworkLoggerRequestGroup nodes (which don't have a valid parent!) + if ( !index.parent().isValid() ) + expandChildren( index ); + + // make ALL request information visible by scrolling view to it + scrollTo( index ); +} + +void QgsNetworkLoggerTreeView::contextMenu( QPoint point ) +{ + const QModelIndex viewModelIndex = indexAt( point ); + const QModelIndex modelIndex = mProxyModel->mapToSource( viewModelIndex ); + + if ( modelIndex.isValid() ) + { + mMenu->clear(); + + const QList< QAction * > actions = mLogger->actions( modelIndex, mMenu ); + mMenu->addActions( actions ); + if ( !mMenu->actions().empty() ) + { + mMenu->exec( viewport()->mapToGlobal( point ) ); + } + } +} + +void QgsNetworkLoggerTreeView::expandChildren( const QModelIndex &index ) +{ + if ( !index.isValid() ) + return; + + const int count = model()->rowCount( index ); + for ( int i = 0; i < count; ++i ) + { + const QModelIndex childIndex = model()->index( i, 0, index ); + expandChildren( childIndex ); + } + if ( !isExpanded( index ) ) + expand( index ); +} + + +// +// QgsNetworkLoggerPanelWidget +// + +QgsNetworkLoggerPanelWidget::QgsNetworkLoggerPanelWidget( QgsNetworkLogger *logger, QWidget *parent ) + : QgsDevToolWidget( parent ) + , mLogger( logger ) +{ + setupUi( this ); + + mTreeView = new QgsNetworkLoggerTreeView( mLogger ); + verticalLayout->addWidget( mTreeView ); + mToolbar->setIconSize( QgsGuiUtils::iconSize( true ) ); + + mFilterLineEdit->setShowClearButton( true ); + mFilterLineEdit->setShowSearchIcon( true ); + mFilterLineEdit->setPlaceholderText( tr( "Filter requests" ) ); + + mActionShowTimeouts->setChecked( true ); + mActionShowSuccessful->setChecked( true ); + mActionRecord->setChecked( mLogger->isLogging() ); + + connect( mFilterLineEdit, &QgsFilterLineEdit::textChanged, mTreeView, &QgsNetworkLoggerTreeView::setFilterString ); + connect( mActionShowTimeouts, &QAction::toggled, mTreeView, &QgsNetworkLoggerTreeView::setShowTimeouts ); + connect( mActionShowSuccessful, &QAction::toggled, mTreeView, &QgsNetworkLoggerTreeView::setShowSuccessful ); + connect( mActionClear, &QAction::triggered, mLogger, &QgsNetworkLogger::clear ); + connect( mActionRecord, &QAction::toggled, this, [ = ]( bool enabled ) + { + QgsSettings().setValue( QStringLiteral( "logNetworkRequests" ), enabled, QgsSettings::App ); + mLogger->enableLogging( enabled ); + } ); + + connect( mLogger, &QAbstractItemModel::rowsInserted, this, [ = ] + { + // we may want to make this optional? + mTreeView->scrollToBottom(); + } ); +} diff --git a/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.h b/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.h new file mode 100644 index 00000000000..92619c0fbc4 --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggerpanelwidget.h @@ -0,0 +1,97 @@ +/*************************************************************************** + qgsnetworkloggerpanelwidget.h + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +#ifndef QGSNETWORKLOGGERPANELWIDGET_H +#define QGSNETWORKLOGGERPANELWIDGET_H + +#include "qgsdevtoolwidget.h" +#include "ui_qgsnetworkloggerpanelbase.h" +#include + +class QgsNetworkLogger; +class QgsNetworkLoggerProxyModel; + +/** + * \ingroup app + * \class QgsNetworkLoggerTreeView + * \brief A custom QTreeView subclass for showing logged network requests. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerTreeView: public QTreeView +{ + Q_OBJECT + public: + + /** + * Constructor for QgsNetworkLoggerTreeView, attached to the specified \a logger. + */ + QgsNetworkLoggerTreeView( QgsNetworkLogger *logger, QWidget *parent = nullptr ); + + public slots: + + /** + * Sets a filter \a string to apply to request URLs. + */ + void setFilterString( const QString &string ); + + /** + * Sets whether successful requests should be shown. + */ + void setShowSuccessful( bool show ); + + /** + * Sets whether timed out requests should be shown. + */ + void setShowTimeouts( bool show ); + + private slots: + void itemExpanded( const QModelIndex &index ); + void contextMenu( QPoint point ); + + private: + + void expandChildren( const QModelIndex &index ); + QMenu *mMenu = nullptr; + QgsNetworkLogger *mLogger = nullptr; + QgsNetworkLoggerProxyModel *mProxyModel = nullptr; +}; + + +/** + * \ingroup app + * \class QgsNetworkLoggerPanelWidget + * \brief A panel widget showing logged network requests. + * + * \since QGIS 3.14 + */ +class QgsNetworkLoggerPanelWidget : public QgsDevToolWidget, private Ui::QgsNetworkLoggerPanelBase +{ + Q_OBJECT + + public: + + /** + * Constructor for QgsNetworkLoggerPanelWidget, linked with the specified \a logger. + */ + QgsNetworkLoggerPanelWidget( QgsNetworkLogger *logger, QWidget *parent ); + + private: + + QgsNetworkLoggerTreeView *mTreeView = nullptr; + QgsNetworkLogger *mLogger = nullptr; +}; + + +#endif // QGSNETWORKLOGGERPANELWIDGET_H diff --git a/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp b/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp new file mode 100644 index 00000000000..acddd281999 --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.cpp @@ -0,0 +1,29 @@ +/*************************************************************************** + qgsnetworkloggerwidgetfactory.cpp + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ + +#include "qgsnetworkloggerwidgetfactory.h" +#include "qgsnetworkloggerpanelwidget.h" +#include "qgsapplication.h" + +QgsNetworkLoggerWidgetFactory::QgsNetworkLoggerWidgetFactory( QgsNetworkLogger *logger ) + : QgsDevToolWidgetFactory( QObject::tr( "Network Logger" ), QgsApplication::getThemeIcon( QStringLiteral( "mIconNetworkLogger.svg" ) ) ) + , mLogger( logger ) +{ +} + +QgsDevToolWidget *QgsNetworkLoggerWidgetFactory::createWidget( QWidget *parent ) const +{ + return new QgsNetworkLoggerPanelWidget( mLogger, parent ); +} diff --git a/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.h b/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.h new file mode 100644 index 00000000000..ebb916c1fbc --- /dev/null +++ b/src/app/devtools/networklogger/qgsnetworkloggerwidgetfactory.h @@ -0,0 +1,35 @@ +/*************************************************************************** + qgsnetworkloggerwidgetfactory.h + ------------------------- + begin : March 2020 + copyright : (C) 2020 by Nyall Dawson + email : nyall dot dawson at gmail dot com + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +#ifndef QGSNETWORKLOGGERWIDGETFACTORY_H +#define QGSNETWORKLOGGERWIDGETFACTORY_H + +#include "qgsdevtoolwidgetfactory.h" + +class QgsNetworkLogger; + +class QgsNetworkLoggerWidgetFactory: public QgsDevToolWidgetFactory +{ + public: + + QgsNetworkLoggerWidgetFactory( QgsNetworkLogger *logger ); + QgsDevToolWidget *createWidget( QWidget *parent = nullptr ) const override; + + private: + + QgsNetworkLogger *mLogger = nullptr; +}; + + +#endif // QGSNETWORKLOGGERWIDGETFACTORY_H diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 44c4eb22d69..88312f1aca7 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -366,9 +366,10 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgsbearingnumericformat.h" #include "qgsprojectdisplaysettings.h" #include "qgstemporalcontrollerdockwidget.h" - +#include "qgsnetworklogger.h" #include "qgsuserprofilemanager.h" #include "qgsuserprofile.h" +#include "qgsnetworkloggerwidgetfactory.h" #include "browser/qgsinbuiltdataitemproviders.h" @@ -832,6 +833,11 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh connect( mUserProfileManager, &QgsUserProfileManager::profilesChanged, this, &QgisApp::refreshProfileMenu ); endProfile(); + // start the network logger early, we want all requests logged! + startProfile( QStringLiteral( "Network logger" ) ); + mNetworkLogger = new QgsNetworkLogger( QgsNetworkAccessManager::instance(), this ); + endProfile(); + // load GUI: actions, menus, toolbars profiler->beginGroup( QStringLiteral( "qgisapp" ) ); profiler->beginGroup( QStringLiteral( "startup" ) ); @@ -1598,6 +1604,9 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh mBearingNumericFormat.reset( QgsLocalDefaultSettings::bearingFormat() ); + mNetworkLoggerWidgetFactory = qgis::make_unique< QgsNetworkLoggerWidgetFactory >( mNetworkLogger ); + registerDevToolFactory( mNetworkLoggerWidgetFactory.get() ); + // update windows qApp->processEvents(); @@ -1690,6 +1699,9 @@ QgisApp::~QgisApp() // shouldn't be needed, but from this stage on, we don't want/need ANY map canvas refreshes to take place mFreezeCount = 1000000; + unregisterDevToolFactory( mNetworkLoggerWidgetFactory.get() ); + mNetworkLoggerWidgetFactory.reset(); + delete mInternalClipboard; delete mQgisInterface; delete mStyleSheetBuilder; diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index 0becd288b81..e0ad51f890f 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -141,6 +141,8 @@ class QgsNetworkRequestParameters; class QgsBearingNumericFormat; class QgsDevToolsPanelWidget; class QgsDevToolWidgetFactory; +class QgsNetworkLogger; +class QgsNetworkLoggerWidgetFactory; #include #include @@ -2440,6 +2442,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow std::unique_ptr< QgsBearingNumericFormat > mBearingNumericFormat; + QgsNetworkLogger *mNetworkLogger = nullptr; + std::unique_ptr< QgsNetworkLoggerWidgetFactory > mNetworkLoggerWidgetFactory; + class QgsCanvasRefreshBlocker { public: diff --git a/src/ui/qgsnetworkloggerpanelbase.ui b/src/ui/qgsnetworkloggerpanelbase.ui new file mode 100644 index 00000000000..733feb8a0c6 --- /dev/null +++ b/src/ui/qgsnetworkloggerpanelbase.ui @@ -0,0 +1,126 @@ + + + QgsNetworkLoggerPanelBase + + + + 0 + 0 + 700 + 629 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 24 + 24 + + + + false + + + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 6 + + + + + + + + + :/images/themes/default/mActionDeleteSelected.svg:/images/themes/default/mActionDeleteSelected.svg + + + Clear + + + Clear Log + + + + + true + + + + :/images/themes/default/mActionPlay.svg:/images/themes/default/mActionPlay.svg + + + Record Log + + + + + true + + + Show Successful Requests + + + + + true + + + Show Timeouts + + + + + + QgsPanelWidget + QWidget +
qgspanelwidget.h
+ 1 +
+ + QgsFilterLineEdit + QLineEdit +
qgsfilterlineedit.h
+
+
+ + + + + + +