Skip to content

Commit 4e96912

Browse files
committedNov 11, 2016
[bugfix][forwardport] File downloader for identify dialog hyperlinks
fixes #14703 Include C++ and Python tests Travis won: ported all test cases to Python and disabled C++ companion test (still useful locally and for debugging) For the curious: QTemporaryFile is not working as expected Moved to Qt5 new style signals Disabled C++ test and connected cancel to progress Make string comparison on SSL errors more robust
1 parent 73b283c commit 4e96912

12 files changed

+856
-1
lines changed
 

‎ci/travis/linux/script.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
2424
# Set OTB application path (installed in before_install.sh script)
2525
export OTB_APPLICATION_PATH=${HOME}/OTB-5.6.0-Linux64/lib/otb/applications
2626

27-
xvfb-run ctest -V -E "qgis_openstreetmaptest|qgis_wcsprovidertest|PyQgsWFSProviderGUI|qgis_ziplayertest|$(cat ${DIR}/blacklist.txt | paste -sd '|' -)" -S ./qgis-test-travis.ctest --output-on-failure
27+
xvfb-run ctest -V -E "qgis_filedownloader|qgis_openstreetmaptest|qgis_wcsprovidertest|PyQgsWFSProviderGUI|qgis_ziplayertest|$(cat ${DIR}/blacklist.txt | paste -sd '|' -)" -S ./qgis-test-travis.ctest --output-on-failure
2828
# xvfb-run ctest -V -E "qgis_openstreetmaptest|qgis_wcsprovidertest" -S ./qgis-test-travis.ctest --output-on-failure

‎python/gui/gui.sip

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
%Include qgsfieldvalidator.sip
7878
%Include qgsfiledropedit.sip
7979
%Include qgsfilewidget.sip
80+
%Include qgsfiledownloader.sip
8081
%Include qgsfilterlineedit.sip
8182
%Include qgsfocuswatcher.sip
8283
%Include qgsformannotationitem.sip

‎python/gui/qgsfiledownloader.sip

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/***************************************************************************
2+
qgsfiledownloader.sip
3+
--------------------------------------
4+
Date : November 2016
5+
Copyright : (C) 2016 by Alessandro Pasotti
6+
Email : elpaso at itopen dot it
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
/** \ingroup gui
17+
* QgsFileDownloader is a utility class for downloading files.
18+
*
19+
* To use this class, it is necessary to pass the URL and an output file name as
20+
* arguments to the constructor, the download will start immediately.
21+
* The download is asynchronous and depending on the guiNotificationsEnabled
22+
* parameter accepted by the constructor (default = true) the class will
23+
* show a progress dialog and report all errors in a QMessageBox::warning dialog.
24+
* If the guiNotificationsEnabled parameter is set to false, the class can still
25+
* be used through the signals and slots mechanism.
26+
* The object will destroy itself when the request completes, errors or is canceled.
27+
*
28+
* @note added in QGIS 2.18.1
29+
*/
30+
class QgsFileDownloader : public QObject
31+
{
32+
%TypeHeaderCode
33+
#include <qgsfiledownloader.h>
34+
%End
35+
public:
36+
/**
37+
* QgsFileDownloader
38+
* @param url the download url
39+
* @param outputFileName file name where the downloaded content will be stored
40+
* @param guiNotificationsEnabled if false, the downloader will not display any progress bar or error message
41+
*/
42+
QgsFileDownloader(QUrl url, QString outputFileName, bool guiNotificationsEnabled = true);
43+
44+
signals:
45+
/** Emitted when the download has completed successfully */
46+
void downloadCompleted();
47+
/** Emitted always when the downloader exits */
48+
void downloadExited();
49+
/** Emitted when the download was canceled by the user */
50+
void downloadCanceled();
51+
/** Emitted when an error makes the download fail */
52+
void downloadError( QStringList errorMessages );
53+
/** Emitted when data ready to be processed */
54+
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
55+
56+
public slots:
57+
/**
58+
* Called when a download is canceled by the user
59+
* this slot aborts the download and deletes the object
60+
* Never call this slot directly: this is meant to
61+
* be managed by the signal-slot system.
62+
*/
63+
void onDownloadCanceled();
64+
65+
private:
66+
~QgsFileDownloader();
67+
68+
};

‎src/app/qgsidentifyresultsdialog.cpp

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
#include "qgswebframe.h"
4343
#include "qgsstringutils.h"
4444
#include "qgstreewidgetitem.h"
45+
#include "qgsfiledownloader.h"
4546

4647
#include <QCloseEvent>
4748
#include <QLabel>
@@ -59,6 +60,11 @@
5960
#include <QMessageBox>
6061
#include <QComboBox>
6162
#include <QTextDocument>
63+
#include <QNetworkRequest>
64+
#include <QNetworkReply>
65+
#include <QFileDialog>
66+
#include <QFileInfo>
67+
#include <QRegExp>
6268

6369
//graph
6470
#include <qwt_plot.h>
@@ -72,13 +78,59 @@ QgsIdentifyResultsWebView::QgsIdentifyResultsWebView( QWidget *parent ) : QgsWeb
7278
setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Minimum );
7379
page()->setNetworkAccessManager( QgsNetworkAccessManager::instance() );
7480
// page()->setLinkDelegationPolicy( QWebPage::DelegateAllLinks );
81+
page()->setForwardUnsupportedContent( true );
7582
page()->setLinkDelegationPolicy( QWebPage::DontDelegateLinks );
7683
settings()->setAttribute( QWebSettings::LocalContentCanAccessRemoteUrls, true );
7784
settings()->setAttribute( QWebSettings::JavascriptCanOpenWindows, true );
7885
settings()->setAttribute( QWebSettings::PluginsEnabled, true );
7986
#ifdef QGISDEBUG
8087
settings()->setAttribute( QWebSettings::DeveloperExtrasEnabled, true );
8188
#endif
89+
connect( page(), SIGNAL( downloadRequested( QNetworkRequest ) ), this, SLOT( downloadRequested( QNetworkRequest ) ) );
90+
connect( page(), SIGNAL( unsupportedContent( QNetworkReply* ) ), this, SLOT( unsupportedContent( QNetworkReply* ) ) );
91+
}
92+
93+
94+
void QgsIdentifyResultsWebView::downloadRequested( const QNetworkRequest &request )
95+
{
96+
handleDownload( request.url() );
97+
}
98+
99+
void QgsIdentifyResultsWebView::unsupportedContent( QNetworkReply * reply )
100+
{
101+
handleDownload( reply->url() );
102+
}
103+
104+
void QgsIdentifyResultsWebView::handleDownload( QUrl url )
105+
{
106+
if ( ! url.isValid() )
107+
{
108+
QMessageBox::warning( this, tr( "Invalid URL" ), tr( "The download URL is not valid: %1" ).arg( url.toString( ) ) );
109+
}
110+
else
111+
{
112+
const QString DOWNLOADER_LAST_DIR_KEY( "Qgis/fileDownloaderLastDir" );
113+
QSettings settings;
114+
// Try to get some information from the URL
115+
QFileInfo info( url.toString( ) );
116+
QString savePath = settings.value( DOWNLOADER_LAST_DIR_KEY ).toString( );
117+
QString fileName = info.fileName().replace( QRegExp( "[^A-z0-9\\-_\\.]" ), "_" );
118+
if ( ! savePath.isEmpty() && ! fileName.isEmpty( ) )
119+
{
120+
savePath = QDir::cleanPath( savePath + QDir::separator() + fileName );
121+
}
122+
QString targetFile = QFileDialog::getSaveFileName( this,
123+
tr( "Save as" ),
124+
savePath,
125+
info.suffix( ).isEmpty() ? QString( ) : "*." + info.suffix( )
126+
);
127+
if ( ! targetFile.isEmpty() )
128+
{
129+
settings.setValue( DOWNLOADER_LAST_DIR_KEY, QFileInfo( targetFile ).dir().absolutePath( ) );
130+
// Start the download
131+
new QgsFileDownloader( url, targetFile );
132+
}
133+
}
82134
}
83135

84136
void QgsIdentifyResultsWebView::print()

‎src/app/qgsidentifyresultsdialog.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828

2929
#include <QWidget>
3030
#include <QList>
31+
#include <QNetworkRequest>
32+
#include <QNetworkReply>
33+
#include <QUrl>
3134

3235
class QCloseEvent;
3336
class QTreeWidgetItem;
@@ -57,9 +60,13 @@ class APP_EXPORT QgsIdentifyResultsWebView : public QgsWebView
5760
QSize sizeHint() const override;
5861
public slots:
5962
void print();
63+
void downloadRequested( const QNetworkRequest &request );
64+
void unsupportedContent( QNetworkReply *reply );
6065
protected:
6166
void contextMenuEvent( QContextMenuEvent* ) override;
6267
QgsWebView *createWindow( QWebPage::WebWindowType type ) override;
68+
private:
69+
void handleDownload( QUrl url );
6370
};
6471

6572
class APP_EXPORT QgsIdentifyResultsFeatureItem: public QTreeWidgetItem

‎src/gui/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ SET(QGIS_GUI_SRCS
317317
qgsuserinputdockwidget.cpp
318318
qgsvariableeditorwidget.cpp
319319
qgsvertexmarker.cpp
320+
qgsfiledownloader.cpp
320321
)
321322

322323
IF (WITH_QTWEBKIT)
@@ -471,6 +472,7 @@ SET(QGIS_GUI_MOC_HDRS
471472
qgsunitselectionwidget.h
472473
qgsuserinputdockwidget.h
473474
qgsvariableeditorwidget.h
475+
qgsfiledownloader.h
474476

475477
raster/qgsmultibandcolorrendererwidget.h
476478
raster/qgspalettedrendererwidget.h
@@ -664,6 +666,7 @@ SET(QGIS_GUI_HDRS
664666
qgsuserinputdockwidget.h
665667
qgsvectorlayertools.h
666668
qgsvertexmarker.h
669+
qgsfiledownloader.h
667670

668671
attributetable/qgsfeaturemodel.h
669672

‎src/gui/qgsfiledownloader.cpp

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
/***************************************************************************
2+
qgsfiledownloader.cpp
3+
--------------------------------------
4+
Date : November 2016
5+
Copyright : (C) 2016 by Alessandro Pasotti
6+
Email : apasotti at boundlessgeo dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include "qgsfiledownloader.h"
17+
#include "qgsnetworkaccessmanager.h"
18+
19+
#include <QNetworkAccessManager>
20+
#include <QNetworkRequest>
21+
#include <QNetworkReply>
22+
#include <QMessageBox>
23+
#ifndef QT_NO_OPENSSL
24+
#include <QSslError>
25+
#endif
26+
27+
QgsFileDownloader::QgsFileDownloader( QUrl url, QString outputFileName, bool enableGuiNotifications )
28+
: mUrl( url )
29+
, mReply( nullptr )
30+
, mProgressDialog( nullptr )
31+
, mDownloadCanceled( false )
32+
, mErrors()
33+
, mGuiNotificationsEnabled( enableGuiNotifications )
34+
{
35+
mFile.setFileName( outputFileName );
36+
startDownload();
37+
}
38+
39+
40+
QgsFileDownloader::~QgsFileDownloader()
41+
{
42+
if ( mReply )
43+
{
44+
mReply->abort();
45+
mReply->deleteLater();
46+
}
47+
if ( mProgressDialog )
48+
{
49+
mProgressDialog->deleteLater();
50+
}
51+
}
52+
53+
54+
void QgsFileDownloader::startDownload()
55+
{
56+
QgsNetworkAccessManager* nam = QgsNetworkAccessManager::instance();
57+
58+
QNetworkRequest request( mUrl );
59+
60+
mReply = nam->get( request );
61+
62+
connect( mReply, &QNetworkReply::readyRead, this, &QgsFileDownloader::onReadyRead );
63+
connect( mReply, static_cast < void ( QNetworkReply::* )( QNetworkReply::NetworkError ) > ( &QNetworkReply::error ), this, &QgsFileDownloader::onNetworkError );
64+
connect( mReply, &QNetworkReply::finished, this, &QgsFileDownloader::onFinished );
65+
connect( mReply, &QNetworkReply::downloadProgress, this, &QgsFileDownloader::onDownloadProgress );
66+
connect( nam, &QgsNetworkAccessManager::requestTimedOut, this, &QgsFileDownloader::onRequestTimedOut );
67+
#ifndef QT_NO_OPENSSL
68+
connect( nam, &QgsNetworkAccessManager::sslErrors, this, &QgsFileDownloader::onSslErrors );
69+
#endif
70+
if ( mGuiNotificationsEnabled )
71+
{
72+
mProgressDialog = new QProgressDialog();
73+
mProgressDialog->setWindowTitle( tr( "Download" ) );
74+
mProgressDialog->setLabelText( tr( "Downloading %1." ).arg( mFile.fileName() ) );
75+
mProgressDialog->show();
76+
connect( mProgressDialog, &QProgressDialog::canceled, this, &QgsFileDownloader::onDownloadCanceled );
77+
}
78+
}
79+
80+
void QgsFileDownloader::onDownloadCanceled()
81+
{
82+
mDownloadCanceled = true;
83+
emit downloadCanceled();
84+
onFinished();
85+
}
86+
87+
void QgsFileDownloader::onRequestTimedOut()
88+
{
89+
error( tr( "Network request %1 timed out" ).arg( mUrl.toString() ) );
90+
}
91+
92+
#ifndef QT_NO_OPENSSL
93+
void QgsFileDownloader::onSslErrors( QNetworkReply *reply, const QList<QSslError> &errors )
94+
{
95+
Q_UNUSED( reply );
96+
QStringList errorMessages;
97+
errorMessages << "SSL Errors: ";
98+
for ( auto end = errors.size(), i = 0; i != end; ++i )
99+
{
100+
errorMessages << errors[i].errorString();
101+
}
102+
error( errorMessages );
103+
}
104+
#endif
105+
106+
107+
void QgsFileDownloader::error( QStringList errorMessages )
108+
{
109+
for ( auto end = errorMessages.size(), i = 0; i != end; ++i )
110+
{
111+
mErrors.append( errorMessages[i] );
112+
}
113+
// Show error
114+
if ( mGuiNotificationsEnabled )
115+
{
116+
QMessageBox::warning( nullptr, tr( "Download failed" ), mErrors.join( "<br>" ) );
117+
}
118+
emit downloadError( mErrors );
119+
}
120+
121+
void QgsFileDownloader::error( QString errorMessage )
122+
{
123+
error( QStringList() << errorMessage );
124+
}
125+
126+
void QgsFileDownloader::onReadyRead()
127+
{
128+
Q_ASSERT( mReply );
129+
if ( ! mFile.isOpen() && ! mFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
130+
{
131+
error( tr( "Cannot open output file: %1" ).arg( mFile.fileName() ) );
132+
onFinished();
133+
}
134+
else
135+
{
136+
QByteArray data = mReply->readAll();
137+
mFile.write( data );
138+
}
139+
}
140+
141+
void QgsFileDownloader::onFinished()
142+
{
143+
// when canceled
144+
if ( ! mErrors.isEmpty() || mDownloadCanceled )
145+
{
146+
mFile.close();
147+
mFile.remove();
148+
if ( mGuiNotificationsEnabled )
149+
mProgressDialog->hide();
150+
}
151+
else
152+
{
153+
// download finished normally
154+
if ( mGuiNotificationsEnabled )
155+
mProgressDialog->hide();
156+
mFile.flush();
157+
mFile.close();
158+
159+
// get redirection url
160+
QVariant redirectionTarget = mReply->attribute( QNetworkRequest::RedirectionTargetAttribute );
161+
if ( mReply->error() )
162+
{
163+
mFile.remove();
164+
error( tr( "Download failed: %1." ).arg( mReply->errorString() ) );
165+
}
166+
else if ( !redirectionTarget.isNull() )
167+
{
168+
QUrl newUrl = mUrl.resolved( redirectionTarget.toUrl() );
169+
mUrl = newUrl;
170+
mReply->deleteLater();
171+
mFile.open( QIODevice::WriteOnly );
172+
mFile.resize( 0 );
173+
mFile.close();
174+
startDownload();
175+
return;
176+
}
177+
// All done
178+
emit downloadCompleted();
179+
}
180+
emit downloadExited();
181+
this->deleteLater();
182+
}
183+
184+
void QgsFileDownloader::onNetworkError( QNetworkReply::NetworkError err )
185+
{
186+
Q_ASSERT( mReply );
187+
error( QString( "Network error %1: %2" ).arg( err ).arg( mReply->errorString() ) );
188+
}
189+
190+
void QgsFileDownloader::onDownloadProgress( qint64 bytesReceived, qint64 bytesTotal )
191+
{
192+
if ( mDownloadCanceled )
193+
{
194+
return;
195+
}
196+
if ( mGuiNotificationsEnabled )
197+
{
198+
mProgressDialog->setMaximum( bytesTotal );
199+
mProgressDialog->setValue( bytesReceived );
200+
}
201+
emit downloadProgress( bytesReceived, bytesTotal );
202+
}
203+

‎src/gui/qgsfiledownloader.h

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/***************************************************************************
2+
qgsfiledownloader.h
3+
--------------------------------------
4+
Date : November 2016
5+
Copyright : (C) 2016 by Alessandro Pasotti
6+
Email : apasotti at boundlessgeo dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#ifndef QGSFILEDOWNLOADER_H
17+
#define QGSFILEDOWNLOADER_H
18+
19+
#include <QObject>
20+
#include <QFile>
21+
#include <QNetworkReply>
22+
#include <QProgressDialog>
23+
#ifndef QT_NO_OPENSSL
24+
#include <QSslError>
25+
#endif
26+
27+
/** \ingroup gui
28+
* QgsFileDownloader is a utility class for downloading files.
29+
*
30+
* To use this class, it is necessary to pass the URL and an output file name as
31+
* arguments to the constructor, the download will start immediately.
32+
* The download is asynchronous and depending on the guiNotificationsEnabled
33+
* parameter accepted by the constructor (default = true) the class will
34+
* show a progress dialog and report all errors in a QMessageBox::warning dialog.
35+
* If the guiNotificationsEnabled parameter is set to false, the class can still
36+
* be used through the signals and slots mechanism.
37+
* The object will destroy itself when the request completes, errors or is canceled.
38+
*
39+
* @note added in QGIS 2.18.1
40+
*/
41+
class GUI_EXPORT QgsFileDownloader : public QObject
42+
{
43+
Q_OBJECT
44+
public:
45+
46+
/**
47+
* QgsFileDownloader
48+
* @param url the download url
49+
* @param outputFileName file name where the downloaded content will be stored
50+
* @param guiNotificationsEnabled if false, the downloader will not display any progress bar or error message
51+
*/
52+
QgsFileDownloader( QUrl url, QString outputFileName, bool guiNotificationsEnabled = true );
53+
54+
signals:
55+
//! Emitted when the download has completed successfully
56+
void downloadCompleted();
57+
//! Emitted always when the downloader exits
58+
void downloadExited();
59+
//! Emitted when the download was canceled by the user
60+
void downloadCanceled();
61+
//! Emitted when an error makes the download fail
62+
void downloadError( QStringList errorMessages );
63+
//! Emitted when data ready to be processed
64+
void downloadProgress( qint64 bytesReceived, qint64 bytesTotal );
65+
66+
public slots:
67+
68+
/**
69+
* Called when a download is canceled by the user
70+
* this slot aborts the download and deletes
71+
* the object.
72+
* Never call this slot directly: this is meant to
73+
* be managed by the signal-slot system.
74+
*/
75+
void onDownloadCanceled();
76+
77+
private slots:
78+
//! Called when the network reply data are ready
79+
void onReadyRead();
80+
//! Called when the network reply has finished
81+
void onFinished();
82+
//! Called on Network Error
83+
void onNetworkError( QNetworkReply::NetworkError err );
84+
//! Called on data ready to be processed
85+
void onDownloadProgress( qint64 bytesReceived, qint64 bytesTotal );
86+
//! Called when a network request times out
87+
void onRequestTimedOut();
88+
//! Called to start the download
89+
void startDownload();
90+
#ifndef QT_NO_OPENSSL
91+
92+
/**
93+
* Called on SSL network Errors
94+
* @param reply
95+
* @param errors
96+
*/
97+
void onSslErrors( QNetworkReply *reply, const QList<QSslError> &errors );
98+
#endif
99+
private:
100+
~QgsFileDownloader();
101+
102+
/**
103+
* Abort current request and show an error if the instance has GUI
104+
* notifications enabled.
105+
*/
106+
void error( QStringList errorMessages );
107+
void error( QString errorMessage );
108+
QUrl mUrl;
109+
QNetworkReply* mReply;
110+
QFile mFile;
111+
QProgressDialog* mProgressDialog;
112+
bool mDownloadCanceled;
113+
QStringList mErrors;
114+
bool mGuiNotificationsEnabled;
115+
};
116+
117+
#endif // QGSFILEDOWNLOADER_H

‎tests/src/gui/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,5 @@ ADD_QGIS_TEST(sqlcomposerdialog testqgssqlcomposerdialog.cpp)
143143
ADD_QGIS_TEST(editorwidgetregistrytest testqgseditorwidgetregistry.cpp)
144144
ADD_QGIS_TEST(keyvaluewidgettest testqgskeyvaluewidget.cpp)
145145
ADD_QGIS_TEST(listwidgettest testqgslistwidget.cpp)
146+
ADD_QGIS_TEST(filedownloader testqgsfiledownloader.cpp)
147+
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
/***************************************************************************
2+
testqgsfilefiledownloader.cpp
3+
--------------------------------------
4+
Date : 11.8.2016
5+
Copyright : (C) 2016 Alessandro Pasotti
6+
Email : apasotti at boundlessgeo dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
17+
#include <QtTest/QtTest>
18+
#include <QObject>
19+
#include <QTemporaryFile>
20+
#include <QUrl>
21+
#include <QEventLoop>
22+
#include <QTimer>
23+
24+
#include <qgsapplication.h>
25+
#include <qgsfiledownloader.h>
26+
27+
class TestQgsFileDownloader: public QObject
28+
{
29+
Q_OBJECT
30+
public:
31+
TestQgsFileDownloader()
32+
: mTempFile( nullptr )
33+
, mErrorMessage()
34+
, mCanceled( false )
35+
, mProgress( false )
36+
, mError( false )
37+
, mCompleted( false )
38+
, mExited( false )
39+
, mFileDownloader( nullptr )
40+
{}
41+
42+
public slots:
43+
//! Called when the download has completed successfully
44+
void downloadCompleted()
45+
{
46+
mCompleted = true;
47+
}
48+
//! Called when the download exits
49+
void downloadExited()
50+
{
51+
mExited = true;
52+
}
53+
//! Called when the download was canceled by the user
54+
void downloadCanceled()
55+
{
56+
mCanceled = true;
57+
}
58+
//! Called when an error makes the download fail
59+
void downloadError( QStringList errorMessages )
60+
{
61+
mError = true;
62+
errorMessages.sort();
63+
mErrorMessage = errorMessages.join( ";" );
64+
}
65+
//! Called when data ready to be processed
66+
void downloadProgress( qint64 bytesReceived, qint64 bytesTotal )
67+
{
68+
Q_UNUSED( bytesReceived );
69+
Q_UNUSED( bytesTotal );
70+
mProgress = true;
71+
}
72+
73+
private slots:
74+
void initTestCase(); // will be called before the first testfunction is executed.
75+
void cleanupTestCase(); // will be called after the last testfunction was executed.
76+
void init(); // will be called before each testfunction is executed.
77+
void cleanup(); // will be called after every testfunction.
78+
79+
void testValidDownload();
80+
void testInValidDownload();
81+
void testCanceledDownload();
82+
void testInvalidFile();
83+
void testInvalidUrl();
84+
void testBlankUrl();
85+
#ifndef QT_NO_OPENSSL
86+
void testSslError_data();
87+
void testSslError();
88+
#endif
89+
90+
private:
91+
void makeCall( QUrl url , QString fileName, bool cancel = false );
92+
QTemporaryFile *mTempFile;
93+
QString mErrorMessage;
94+
bool mCanceled;
95+
bool mProgress;
96+
bool mError;
97+
bool mCompleted;
98+
bool mExited;
99+
QgsFileDownloader *mFileDownloader;
100+
};
101+
102+
void TestQgsFileDownloader::makeCall( QUrl url, QString fileName, bool cancel )
103+
{
104+
QEventLoop loop;
105+
106+
mFileDownloader = new QgsFileDownloader( url, fileName, false );
107+
connect( mFileDownloader, &QgsFileDownloader::downloadCompleted, this, &TestQgsFileDownloader::downloadCompleted );
108+
connect( mFileDownloader, &QgsFileDownloader::downloadCanceled, this, &TestQgsFileDownloader::downloadCanceled );
109+
connect( mFileDownloader, &QgsFileDownloader::downloadExited, this, &TestQgsFileDownloader::downloadExited );
110+
connect( mFileDownloader, &QgsFileDownloader::downloadError, this, &TestQgsFileDownloader::downloadError );
111+
connect( mFileDownloader, &QgsFileDownloader::downloadProgress, this, &TestQgsFileDownloader::downloadProgress );
112+
113+
connect( mFileDownloader, &QgsFileDownloader::downloadExited, &loop, &QEventLoop::quit );
114+
115+
if ( cancel )
116+
QTimer::singleShot( 1000, mFileDownloader, &QgsFileDownloader::onDownloadCanceled );
117+
118+
loop.exec();
119+
120+
}
121+
122+
void TestQgsFileDownloader::initTestCase()
123+
{
124+
QgsApplication::init();
125+
QgsApplication::initQgis();
126+
127+
}
128+
129+
void TestQgsFileDownloader::cleanupTestCase()
130+
{
131+
QgsApplication::exitQgis();
132+
}
133+
134+
void TestQgsFileDownloader::init()
135+
{
136+
mErrorMessage.clear();
137+
mCanceled = false;
138+
mProgress = false;
139+
mError = false;
140+
mCompleted = false;
141+
mExited = false;
142+
mTempFile = new QTemporaryFile( );
143+
Q_ASSERT( mTempFile->open() );
144+
mTempFile->close();
145+
}
146+
147+
148+
149+
void TestQgsFileDownloader::cleanup()
150+
{
151+
delete mTempFile;
152+
}
153+
154+
void TestQgsFileDownloader::testValidDownload()
155+
{
156+
QVERIFY( ! mTempFile->fileName().isEmpty() );
157+
makeCall( QUrl( "http://www.qgis.org" ), mTempFile->fileName() );
158+
QVERIFY( mExited );
159+
QVERIFY( mCompleted );
160+
QVERIFY( mProgress );
161+
QVERIFY( !mError );
162+
QVERIFY( !mCanceled );
163+
QVERIFY( mTempFile->size() > 0 );
164+
}
165+
166+
void TestQgsFileDownloader::testInValidDownload()
167+
{
168+
QVERIFY( ! mTempFile->fileName().isEmpty() );
169+
makeCall( QUrl( "http://www.doesnotexistofthatimsure.qgis" ), mTempFile->fileName() );
170+
QVERIFY( mExited );
171+
QVERIFY( !mCompleted );
172+
QVERIFY( mError );
173+
QVERIFY( !mCanceled );
174+
QVERIFY( mTempFile->size() == 0 );
175+
QCOMPARE( mErrorMessage, QString( "Network error 3: Host www.doesnotexistofthatimsure.qgis not found" ) );
176+
}
177+
178+
void TestQgsFileDownloader::testCanceledDownload()
179+
{
180+
QVERIFY( ! mTempFile->fileName().isEmpty() );
181+
makeCall( QUrl( "https://github.com/qgis/QGIS/archive/master.zip" ), mTempFile->fileName(), true );
182+
QVERIFY( mExited );
183+
QVERIFY( !mCompleted );
184+
QVERIFY( !mError );
185+
QVERIFY( mProgress );
186+
QVERIFY( mCanceled );
187+
QVERIFY( mTempFile->size() == 0 );
188+
}
189+
190+
void TestQgsFileDownloader::testInvalidFile()
191+
{
192+
makeCall( QUrl( "https://github.com/qgis/QGIS/archive/master.zip" ), QString() );
193+
QVERIFY( mExited );
194+
QVERIFY( !mCompleted );
195+
QVERIFY( mError );
196+
QVERIFY( !mCanceled );
197+
QCOMPARE( mErrorMessage, QString( "Cannot open output file: " ) );
198+
}
199+
200+
void TestQgsFileDownloader::testInvalidUrl()
201+
{
202+
QVERIFY( ! mTempFile->fileName().isEmpty() );
203+
makeCall( QUrl( "xyz://www" ), mTempFile->fileName() );
204+
QVERIFY( mExited );
205+
QVERIFY( !mCompleted );
206+
QVERIFY( mError );
207+
QVERIFY( !mCanceled );
208+
QCOMPARE( mErrorMessage, QString( "Network error 301: Protocol \"xyz\" is unknown" ) );
209+
}
210+
211+
void TestQgsFileDownloader::testBlankUrl()
212+
{
213+
QVERIFY( ! mTempFile->fileName().isEmpty() );
214+
makeCall( QUrl( "" ), mTempFile->fileName() );
215+
QVERIFY( mExited );
216+
QVERIFY( !mCompleted );
217+
QVERIFY( mError );
218+
QVERIFY( !mCanceled );
219+
QCOMPARE( mErrorMessage, QString( "Network error 301: Protocol \"\" is unknown" ) );
220+
}
221+
222+
#ifndef QT_NO_OPENSSL
223+
void TestQgsFileDownloader::testSslError_data()
224+
{
225+
QTest::addColumn<QString>( "url" );
226+
QTest::addColumn<QString>( "result" );
227+
228+
QTest::newRow( "expired" ) << "https://expired.badssl.com/"
229+
<< "Network error 6: SSL handshake failed;SSL Errors: ;The certificate has expired";
230+
QTest::newRow( "self-signed" ) << "https://self-signed.badssl.com/"
231+
<< "Network error 6: SSL handshake failed;SSL Errors: ;The certificate is self-signed, and untrusted";
232+
QTest::newRow( "untrusted-root" ) << "https://untrusted-root.badssl.com/"
233+
<< "Network error 6: SSL handshake failed;No certificates could be verified;SSL Errors: ;The issuer certificate of a locally looked up certificate could not be found";
234+
}
235+
236+
void TestQgsFileDownloader::testSslError()
237+
{
238+
QFETCH( QString, url );
239+
QFETCH( QString, result );
240+
QVERIFY( ! mTempFile->fileName().isEmpty() );
241+
makeCall( QUrl( url ), mTempFile->fileName() );
242+
QCOMPARE( mErrorMessage, result );
243+
QVERIFY( !mCompleted );
244+
QVERIFY( mError );
245+
QVERIFY( !mCanceled );
246+
}
247+
248+
#endif
249+
250+
251+
QTEST_MAIN( TestQgsFileDownloader )
252+
#include "testqgsfiledownloader.moc"
253+
254+

‎tests/src/python/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ ADD_PYTHON_TEST(PyQgsConsole test_console.py)
123123
ADD_PYTHON_TEST(PyQgsLayerDependencies test_layer_dependencies.py)
124124
ADD_PYTHON_TEST(PyQgsVersionCompare test_versioncompare.py)
125125
ADD_PYTHON_TEST(PyQgsDBManagerGpkg test_db_manager_gpkg.py)
126+
ADD_PYTHON_TEST(PyQgsFileDownloader test_qgsfiledownloader.py)
126127

127128
IF (NOT WIN32)
128129
ADD_PYTHON_TEST(PyQgsLogger test_qgslogger.py)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Test the QgsFileDownloader class
4+
5+
.. note:: This program is free software; you can redistribute it and/or modify
6+
it under the terms of the GNU General Public License as published by
7+
the Free Software Foundation; either version 2 of the License, or
8+
(at your option) any later version.
9+
"""
10+
from __future__ import print_function
11+
from future import standard_library
12+
import os
13+
import tempfile
14+
from functools import partial
15+
from qgis.PyQt.QtCore import QEventLoop, QUrl, QTimer
16+
from qgis.gui import (QgsFileDownloader,)
17+
from qgis.testing import start_app, unittest
18+
19+
standard_library.install_aliases()
20+
21+
__author__ = 'Alessandro Pasotti'
22+
__date__ = '08/11/2016'
23+
__copyright__ = 'Copyright 2016, The QGIS Project'
24+
# This will get replaced with a git SHA1 when you do a git archive
25+
__revision__ = '$Format:%H$'
26+
27+
28+
start_app()
29+
30+
31+
class TestQgsFileDownloader(unittest.TestCase):
32+
33+
"""
34+
This class tests the QgsFileDownloader class
35+
"""
36+
37+
def _make_download(self, url, destination, cancel=False):
38+
self.completed_was_called = False
39+
self.error_was_called = False
40+
self.canceled_was_called = False
41+
self.progress_was_called = False
42+
self.exited_was_called = False
43+
44+
loop = QEventLoop()
45+
46+
downloader = QgsFileDownloader(QUrl(url), destination, False)
47+
downloader.downloadCompleted.connect(partial(self._set_slot, 'completed'))
48+
downloader.downloadExited.connect(partial(self._set_slot, 'exited'))
49+
downloader.downloadCanceled.connect(partial(self._set_slot, 'canceled'))
50+
downloader.downloadError.connect(partial(self._set_slot, 'error'))
51+
downloader.downloadProgress.connect(partial(self._set_slot, 'progress'))
52+
53+
downloader.downloadExited.connect(loop.quit)
54+
55+
if cancel:
56+
downloader.downloadProgress.connect(downloader.onDownloadCanceled)
57+
58+
loop.exec_()
59+
60+
def test_validDownload(self):
61+
"""Tests a valid download"""
62+
destination = tempfile.mktemp()
63+
self._make_download('http://www.qgis.org', destination)
64+
self.assertTrue(self.exited_was_called)
65+
self.assertTrue(self.completed_was_called)
66+
self.assertTrue(self.progress_was_called)
67+
self.assertFalse(self.canceled_was_called)
68+
self.assertFalse(self.error_was_called)
69+
self.assertTrue(os.path.isfile(destination))
70+
self.assertGreater(os.path.getsize(destination), 0)
71+
72+
def test_inValidDownload(self):
73+
"""Tests an invalid download"""
74+
destination = tempfile.mktemp()
75+
self._make_download('http://www.doesnotexistofthatimsure.qgis', destination)
76+
self.assertTrue(self.exited_was_called)
77+
self.assertFalse(self.completed_was_called)
78+
self.assertTrue(self.progress_was_called)
79+
self.assertFalse(self.canceled_was_called)
80+
self.assertTrue(self.error_was_called)
81+
self.assertEqual(self.error_args[1], [u'Network error 3: Host www.doesnotexistofthatimsure.qgis not found'])
82+
self.assertFalse(os.path.isfile(destination))
83+
84+
def test_dowloadCanceled(self):
85+
"""Tests user canceled download"""
86+
destination = tempfile.mktemp()
87+
self._make_download('https://github.com/qgis/QGIS/archive/master.zip', destination, True)
88+
self.assertTrue(self.exited_was_called)
89+
self.assertFalse(self.completed_was_called)
90+
self.assertTrue(self.canceled_was_called)
91+
self.assertFalse(self.error_was_called)
92+
self.assertFalse(os.path.isfile(destination))
93+
94+
def test_InvalidUrl(self):
95+
destination = tempfile.mktemp()
96+
self._make_download('xyz://www', destination)
97+
self.assertTrue(self.exited_was_called)
98+
self.assertFalse(self.completed_was_called)
99+
self.assertFalse(self.canceled_was_called)
100+
self.assertTrue(self.error_was_called)
101+
self.assertFalse(os.path.isfile(destination))
102+
self.assertEqual(self.error_args[1], [u"Network error 301: Protocol \"xyz\" is unknown"])
103+
104+
def test_InvalidFile(self):
105+
self._make_download('https://github.com/qgis/QGIS/archive/master.zip', "")
106+
self.assertTrue(self.exited_was_called)
107+
self.assertFalse(self.completed_was_called)
108+
self.assertFalse(self.canceled_was_called)
109+
self.assertTrue(self.error_was_called)
110+
self.assertEqual(self.error_args[1], [u"Cannot open output file: "])
111+
112+
def test_BlankUrl(self):
113+
destination = tempfile.mktemp()
114+
self._make_download('', destination)
115+
self.assertTrue(self.exited_was_called)
116+
self.assertFalse(self.completed_was_called)
117+
self.assertFalse(self.canceled_was_called)
118+
self.assertTrue(self.error_was_called)
119+
self.assertFalse(os.path.isfile(destination))
120+
self.assertEqual(self.error_args[1], [u"Network error 301: Protocol \"\" is unknown"])
121+
122+
def ssl_compare(self, name, url, error):
123+
destination = tempfile.mktemp()
124+
self._make_download(url, destination)
125+
msg = "Failed in %s: %s" % (name, url)
126+
self.assertTrue(self.exited_was_called)
127+
self.assertFalse(self.completed_was_called, msg)
128+
self.assertFalse(self.canceled_was_called, msg)
129+
self.assertTrue(self.error_was_called, msg)
130+
self.assertFalse(os.path.isfile(destination), msg)
131+
result = sorted(self.error_args[1])
132+
result = ';'.join(result)
133+
self.assertTrue(result.startswith(error), msg + "expected:\n%s\nactual:\n%s\n" % (result, error))
134+
135+
def test_sslExpired(self):
136+
self.ssl_compare("expired", "https://expired.badssl.com/", "Network error 6: SSL handshake failed;SSL Errors: ;The certificate has expired")
137+
self.ssl_compare("self-signed", "https://self-signed.badssl.com/", "Network error 6: SSL handshake failed;SSL Errors: ;The certificate is self-signed, and untrusted")
138+
self.ssl_compare("untrusted-root", "https://untrusted-root.badssl.com/", "Network error 6: SSL handshake failed;No certificates could be verified;")
139+
140+
def _set_slot(self, *args, **kwargs):
141+
#print('_set_slot(%s) called' % args[0])
142+
setattr(self, args[0] + '_was_called', True)
143+
setattr(self, args[0] + '_args', args)
144+
145+
146+
if __name__ == '__main__':
147+
unittest.main()

0 commit comments

Comments
 (0)
Please sign in to comment.