Skip to content

Commit 66f4ff9

Browse files
authored
Merge pull request #4407 from boundlessgeo/filedownloader-auth
[auth] Add authentication configuration support to QgsFileDownloader
2 parents c869fa2 + ae7ace9 commit 66f4ff9

File tree

6 files changed

+191
-65
lines changed

6 files changed

+191
-65
lines changed

python/auto_sip.blacklist

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,6 @@ gui/qgsfeatureselectiondlg.sip
337337
gui/qgsfieldvalidator.sip
338338
gui/qgsfieldvalueslineedit.sip
339339
gui/qgsfiledropedit.sip
340-
gui/qgsfiledownloader.sip
341340
gui/qgsfilterlineedit.sip
342341
gui/qgsfloatingwidget.sip
343342
gui/qgsfocuswatcher.sip

python/gui/qgsfiledownloader.sip

Lines changed: 79 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,87 @@
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-
***************************************************************************/
1+
/************************************************************************
2+
* This file has been generated automatically from *
3+
* *
4+
* src/gui/qgsfiledownloader.h *
5+
* *
6+
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
7+
************************************************************************/
158

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
9+
10+
11+
class QgsFileDownloader : QObject
3112
{
32-
%TypeHeaderCode
33-
#include <qgsfiledownloader.h>
34-
%End
13+
%Docstring
14+
QgsFileDownloader is a utility class for downloading files.
15+
16+
To use this class, it is necessary to pass the URL and an output file name as
17+
arguments to the constructor, the download will start immediately.
18+
The download is asynchronous and depending on the guiNotificationsEnabled
19+
parameter accepted by the constructor (default = true) the class will
20+
show a progress dialog and report all errors in a QMessageBox.warning dialog.
21+
If the guiNotificationsEnabled parameter is set to false, the class can still
22+
be used through the signals and slots mechanism.
23+
The object will destroy itself when the request completes, errors or is canceled.
24+
An optional authentication configuration can be specified.
25+
26+
.. versionadded:: 2.18.1
27+
%End
28+
29+
%TypeHeaderCode
30+
#include "qgsfiledownloader.h"
31+
%End
3532
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);
4333

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);
34+
QgsFileDownloader( const QUrl &url, const QString &outputFileName, bool guiNotificationsEnabled = true, QString authcfg = QString() );
35+
%Docstring
36+
QgsFileDownloader
37+
\param url the download url
38+
\param outputFileName file name where the downloaded content will be stored
39+
\param guiNotificationsEnabled if false, the downloader will not display any progress bar or error message
40+
\param authcfg optionally apply this authentication configuration
41+
%End
42+
43+
signals:
44+
void downloadCompleted();
45+
%Docstring
46+
Emitted when the download has completed successfully
47+
%End
48+
void downloadExited();
49+
%Docstring
50+
Emitted always when the downloader exits
51+
%End
52+
void downloadCanceled();
53+
%Docstring
54+
Emitted when the download was canceled by the user
55+
%End
56+
void downloadError( QStringList errorMessages );
57+
%Docstring
58+
Emitted when an error makes the download fail
59+
%End
60+
void downloadProgress( qint64 bytesReceived, qint64 bytesTotal );
61+
%Docstring
62+
Emitted when data are ready to be processed
63+
%End
5564

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();
65+
public slots:
6466

65-
private:
66-
~QgsFileDownloader();
67+
void onDownloadCanceled();
68+
%Docstring
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+
%End
75+
76+
protected:
77+
~QgsFileDownloader();
6778

6879
};
80+
81+
/************************************************************************
82+
* This file has been generated automatically from *
83+
* *
84+
* src/gui/qgsfiledownloader.h *
85+
* *
86+
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
87+
************************************************************************/

src/gui/qgsfiledownloader.cpp

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "qgsfiledownloader.h"
1717
#include "qgsnetworkaccessmanager.h"
1818
#include "qgsapplication.h"
19+
#include "qgsauthmanager.h"
1920

2021
#include <QNetworkAccessManager>
2122
#include <QNetworkRequest>
@@ -25,15 +26,17 @@
2526
#include <QSslError>
2627
#endif
2728

28-
QgsFileDownloader::QgsFileDownloader( const QUrl &url, const QString &outputFileName, bool enableGuiNotifications )
29+
QgsFileDownloader::QgsFileDownloader( const QUrl &url, const QString &outputFileName, bool enableGuiNotifications, QString authcfg )
2930
: mUrl( url )
3031
, mReply( nullptr )
3132
, mProgressDialog( nullptr )
3233
, mDownloadCanceled( false )
3334
, mErrors()
3435
, mGuiNotificationsEnabled( enableGuiNotifications )
36+
, mAuthCfg( )
3537
{
3638
mFile.setFileName( outputFileName );
39+
mAuthCfg = authcfg;
3740
startDownload();
3841
}
3942

@@ -57,6 +60,11 @@ void QgsFileDownloader::startDownload()
5760
QgsNetworkAccessManager *nam = QgsNetworkAccessManager::instance();
5861

5962
QNetworkRequest request( mUrl );
63+
if ( !mAuthCfg.isEmpty() )
64+
{
65+
QgsAuthManager::instance()->updateNetworkRequest( request, mAuthCfg );
66+
}
67+
6068
if ( mReply )
6169
{
6270
disconnect( mReply, &QNetworkReply::readyRead, this, &QgsFileDownloader::onReadyRead );
@@ -65,7 +73,12 @@ void QgsFileDownloader::startDownload()
6573
mReply->abort();
6674
mReply->deleteLater();
6775
}
76+
6877
mReply = nam->get( request );
78+
if ( !mAuthCfg.isEmpty() )
79+
{
80+
QgsAuthManager::instance()->updateNetworkReply( mReply, mAuthCfg );
81+
}
6982

7083
connect( mReply, &QNetworkReply::readyRead, this, &QgsFileDownloader::onReadyRead );
7184
connect( mReply, &QNetworkReply::finished, this, &QgsFileDownloader::onFinished );

src/gui/qgsfiledownloader.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
* If the guiNotificationsEnabled parameter is set to false, the class can still
3737
* be used through the signals and slots mechanism.
3838
* The object will destroy itself when the request completes, errors or is canceled.
39+
* An optional authentication configuration can be specified.
3940
*
4041
* \since QGIS 2.18.1
4142
*/
@@ -49,8 +50,9 @@ class GUI_EXPORT QgsFileDownloader : public QObject
4950
* \param url the download url
5051
* \param outputFileName file name where the downloaded content will be stored
5152
* \param guiNotificationsEnabled if false, the downloader will not display any progress bar or error message
53+
* \param authcfg optionally apply this authentication configuration
5254
*/
53-
QgsFileDownloader( const QUrl &url, const QString &outputFileName, bool guiNotificationsEnabled = true );
55+
QgsFileDownloader( const QUrl &url, const QString &outputFileName, bool guiNotificationsEnabled = true, QString authcfg = QString() );
5456

5557
signals:
5658
//! Emitted when the download has completed successfully
@@ -61,7 +63,7 @@ class GUI_EXPORT QgsFileDownloader : public QObject
6163
void downloadCanceled();
6264
//! Emitted when an error makes the download fail
6365
void downloadError( QStringList errorMessages );
64-
//! Emitted when data ready to be processed
66+
//! Emitted when data are ready to be processed
6567
void downloadProgress( qint64 bytesReceived, qint64 bytesTotal );
6668

6769
public slots:
@@ -114,6 +116,7 @@ class GUI_EXPORT QgsFileDownloader : public QObject
114116
bool mDownloadCanceled;
115117
QStringList mErrors;
116118
bool mGuiNotificationsEnabled;
119+
QString mAuthCfg;
117120
};
118121

119122
#endif // QGSFILEDOWNLOADER_H

tests/src/python/test_authmanager_password_ows.py

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
configuration to access an HTTP Basic protected endpoint.
99
1010
11-
From build dir, run: ctest -R PyQgsAuthManagerPasswordOWSTest -V
11+
From build dir, run from test directory:
12+
LC_ALL=EN ctest -R PyQgsAuthManagerPasswordOWSTest -V
1213
1314
.. note:: This program is free software; you can redistribute it and/or modify
1415
it under the terms of the GNU General Public License as published by
@@ -23,6 +24,7 @@
2324
import random
2425
import string
2526
import urllib
27+
from functools import partial
2628

2729
__author__ = 'Alessandro Pasotti'
2830
__date__ = '18/09/2016'
@@ -39,10 +41,17 @@
3941
QgsVectorLayer,
4042
QgsRasterLayer,
4143
)
44+
from qgis.gui import (
45+
QgsFileDownloader,
46+
)
4247
from qgis.testing import (
4348
start_app,
4449
unittest,
4550
)
51+
from qgis.PyQt.QtCore import (
52+
QEventLoop,
53+
QUrl,
54+
)
4655

4756
try:
4857
QGIS_SERVER_ENDPOINT_PORT = os.environ['QGIS_SERVER_ENDPOINT_PORT']
@@ -180,6 +189,86 @@ def testInvalidAuthAccess(self):
180189
wms_layer = self._getWMSLayer('testlayer_èé')
181190
self.assertFalse(wms_layer.isValid())
182191

192+
def testInvalidAuthFileDownload(self):
193+
"""
194+
Download a protected map tile without authcfg
195+
"""
196+
qs = "?" + "&".join(["%s=%s" % i for i in list({
197+
"MAP": urllib.parse.quote(self.project_path),
198+
"SERVICE": "WMS",
199+
"VERSION": "1.1.1",
200+
"REQUEST": "GetMap",
201+
"LAYERS": "testlayer_èé".replace('_', '%20'),
202+
"STYLES": "",
203+
"FORMAT": "image/png",
204+
"BBOX": "-16817707,-4710778,5696513,14587125",
205+
"HEIGHT": "500",
206+
"WIDTH": "500",
207+
"CRS": "EPSG:3857"
208+
}.items())])
209+
url = '%s://%s:%s/%s' % (self.protocol, self.hostname, self.port, qs)
210+
211+
destination = tempfile.mktemp()
212+
loop = QEventLoop()
213+
214+
downloader = QgsFileDownloader(QUrl(url), destination, False)
215+
downloader.downloadCompleted.connect(partial(self._set_slot, 'completed'))
216+
downloader.downloadExited.connect(partial(self._set_slot, 'exited'))
217+
downloader.downloadCanceled.connect(partial(self._set_slot, 'canceled'))
218+
downloader.downloadError.connect(partial(self._set_slot, 'error'))
219+
downloader.downloadProgress.connect(partial(self._set_slot, 'progress'))
220+
221+
downloader.downloadExited.connect(loop.quit)
222+
223+
loop.exec_()
224+
225+
self.assertTrue(self.error_was_called)
226+
self.assertTrue("Download failed: Host requires authentication" in str(self.error_args), "Error args is: %s" % str(self.error_args))
227+
228+
def testValidAuthFileDownload(self):
229+
"""
230+
Download a map tile with valid authcfg
231+
"""
232+
qs = "?" + "&".join(["%s=%s" % i for i in list({
233+
"MAP": urllib.parse.quote(self.project_path),
234+
"SERVICE": "WMS",
235+
"VERSION": "1.1.1",
236+
"REQUEST": "GetMap",
237+
"LAYERS": "testlayer_èé".replace('_', '%20'),
238+
"STYLES": "",
239+
"FORMAT": "image/png",
240+
"BBOX": "-16817707,-4710778,5696513,14587125",
241+
"HEIGHT": "500",
242+
"WIDTH": "500",
243+
"CRS": "EPSG:3857"
244+
}.items())])
245+
url = '%s://%s:%s/%s' % (self.protocol, self.hostname, self.port, qs)
246+
247+
destination = tempfile.mktemp()
248+
loop = QEventLoop()
249+
250+
downloader = QgsFileDownloader(QUrl(url), destination, False, self.auth_config.id())
251+
downloader.downloadCompleted.connect(partial(self._set_slot, 'completed'))
252+
downloader.downloadExited.connect(partial(self._set_slot, 'exited'))
253+
downloader.downloadCanceled.connect(partial(self._set_slot, 'canceled'))
254+
downloader.downloadError.connect(partial(self._set_slot, 'error'))
255+
downloader.downloadProgress.connect(partial(self._set_slot, 'progress'))
256+
257+
downloader.downloadExited.connect(loop.quit)
258+
259+
loop.exec_()
260+
261+
# Check the we've got a likely PNG image
262+
self.assertTrue(self.completed_was_called)
263+
self.assertTrue(os.path.getsize(destination) > 700000, "Image size: %s" % os.path.getsize(destination)) # > 1MB
264+
with open(destination, 'rb') as f:
265+
self.assertTrue(b'PNG' in f.read()) # is a PNG
266+
267+
def _set_slot(self, *args, **kwargs):
268+
#print('_set_slot(%s) called' % args[0])
269+
setattr(self, args[0] + '_was_called', True)
270+
setattr(self, args[0] + '_args', args)
271+
183272

184273
if __name__ == '__main__':
185274
unittest.main()

tests/src/python/test_qgsfiledownloader.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
"""
33
Test the QgsFileDownloader class
44
5+
Run test with:
6+
LC_ALL=EN ctest -V -R PyQgsFileDownloader
7+
58
.. note:: This program is free software; you can redistribute it and/or modify
69
it under the terms of the GNU General Public License as published by
710
the Free Software Foundation; either version 2 of the License, or

0 commit comments

Comments
 (0)