Skip to content
Permalink
Browse files
Merge pull request #4407 from boundlessgeo/filedownloader-auth
[auth] Add authentication configuration support to QgsFileDownloader
  • Loading branch information
dakcarto committed Apr 25, 2017
2 parents c869fa2 + ae7ace9 commit 66f4ff9710a4027ad48cdfbffd097f30363ad3af
@@ -337,7 +337,6 @@ gui/qgsfeatureselectiondlg.sip
gui/qgsfieldvalidator.sip
gui/qgsfieldvalueslineedit.sip
gui/qgsfiledropedit.sip
gui/qgsfiledownloader.sip
gui/qgsfilterlineedit.sip
gui/qgsfloatingwidget.sip
gui/qgsfocuswatcher.sip
@@ -1,68 +1,87 @@
/***************************************************************************
qgsfiledownloader.sip
--------------------------------------
Date : November 2016
Copyright : (C) 2016 by Alessandro Pasotti
Email : elpaso at itopen dot it
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/qgsfiledownloader.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/

/** \ingroup gui
* QgsFileDownloader is a utility class for downloading files.
*
* To use this class, it is necessary to pass the URL and an output file name as
* arguments to the constructor, the download will start immediately.
* The download is asynchronous and depending on the guiNotificationsEnabled
* parameter accepted by the constructor (default = true) the class will
* show a progress dialog and report all errors in a QMessageBox::warning dialog.
* If the guiNotificationsEnabled parameter is set to false, the class can still
* be used through the signals and slots mechanism.
* The object will destroy itself when the request completes, errors or is canceled.
*
* @note added in QGIS 2.18.1
*/
class QgsFileDownloader : public QObject


class QgsFileDownloader : QObject
{
%TypeHeaderCode
#include <qgsfiledownloader.h>
%End
%Docstring
QgsFileDownloader is a utility class for downloading files.

To use this class, it is necessary to pass the URL and an output file name as
arguments to the constructor, the download will start immediately.
The download is asynchronous and depending on the guiNotificationsEnabled
parameter accepted by the constructor (default = true) the class will
show a progress dialog and report all errors in a QMessageBox.warning dialog.
If the guiNotificationsEnabled parameter is set to false, the class can still
be used through the signals and slots mechanism.
The object will destroy itself when the request completes, errors or is canceled.
An optional authentication configuration can be specified.

.. versionadded:: 2.18.1
%End

%TypeHeaderCode
#include "qgsfiledownloader.h"
%End
public:
/**
* QgsFileDownloader
* @param url the download url
* @param outputFileName file name where the downloaded content will be stored
* @param guiNotificationsEnabled if false, the downloader will not display any progress bar or error message
*/
QgsFileDownloader(QUrl url, QString outputFileName, bool guiNotificationsEnabled = true);

signals:
/** Emitted when the download has completed successfully */
void downloadCompleted();
/** Emitted always when the downloader exits */
void downloadExited();
/** Emitted when the download was canceled by the user */
void downloadCanceled();
/** Emitted when an error makes the download fail */
void downloadError( QStringList errorMessages );
/** Emitted when data ready to be processed */
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
QgsFileDownloader( const QUrl &url, const QString &outputFileName, bool guiNotificationsEnabled = true, QString authcfg = QString() );
%Docstring
QgsFileDownloader
\param url the download url
\param outputFileName file name where the downloaded content will be stored
\param guiNotificationsEnabled if false, the downloader will not display any progress bar or error message
\param authcfg optionally apply this authentication configuration
%End

signals:
void downloadCompleted();
%Docstring
Emitted when the download has completed successfully
%End
void downloadExited();
%Docstring
Emitted always when the downloader exits
%End
void downloadCanceled();
%Docstring
Emitted when the download was canceled by the user
%End
void downloadError( QStringList errorMessages );
%Docstring
Emitted when an error makes the download fail
%End
void downloadProgress( qint64 bytesReceived, qint64 bytesTotal );
%Docstring
Emitted when data are ready to be processed
%End

public slots:
/**
* Called when a download is canceled by the user
* this slot aborts the download and deletes the object
* Never call this slot directly: this is meant to
* be managed by the signal-slot system.
*/
void onDownloadCanceled();
public slots:

private:
~QgsFileDownloader();
void onDownloadCanceled();
%Docstring
Called when a download is canceled by the user
this slot aborts the download and deletes
the object.
Never call this slot directly: this is meant to
be managed by the signal-slot system.
%End

protected:
~QgsFileDownloader();

};

/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/qgsfiledownloader.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.pl again *
************************************************************************/
@@ -16,6 +16,7 @@
#include "qgsfiledownloader.h"
#include "qgsnetworkaccessmanager.h"
#include "qgsapplication.h"
#include "qgsauthmanager.h"

#include <QNetworkAccessManager>
#include <QNetworkRequest>
@@ -25,15 +26,17 @@
#include <QSslError>
#endif

QgsFileDownloader::QgsFileDownloader( const QUrl &url, const QString &outputFileName, bool enableGuiNotifications )
QgsFileDownloader::QgsFileDownloader( const QUrl &url, const QString &outputFileName, bool enableGuiNotifications, QString authcfg )
: mUrl( url )
, mReply( nullptr )
, mProgressDialog( nullptr )
, mDownloadCanceled( false )
, mErrors()
, mGuiNotificationsEnabled( enableGuiNotifications )
, mAuthCfg( )
{
mFile.setFileName( outputFileName );
mAuthCfg = authcfg;
startDownload();
}

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

QNetworkRequest request( mUrl );
if ( !mAuthCfg.isEmpty() )
{
QgsAuthManager::instance()->updateNetworkRequest( request, mAuthCfg );
}

if ( mReply )
{
disconnect( mReply, &QNetworkReply::readyRead, this, &QgsFileDownloader::onReadyRead );
@@ -65,7 +73,12 @@ void QgsFileDownloader::startDownload()
mReply->abort();
mReply->deleteLater();
}

mReply = nam->get( request );
if ( !mAuthCfg.isEmpty() )
{
QgsAuthManager::instance()->updateNetworkReply( mReply, mAuthCfg );
}

connect( mReply, &QNetworkReply::readyRead, this, &QgsFileDownloader::onReadyRead );
connect( mReply, &QNetworkReply::finished, this, &QgsFileDownloader::onFinished );
@@ -36,6 +36,7 @@
* If the guiNotificationsEnabled parameter is set to false, the class can still
* be used through the signals and slots mechanism.
* The object will destroy itself when the request completes, errors or is canceled.
* An optional authentication configuration can be specified.
*
* \since QGIS 2.18.1
*/
@@ -49,8 +50,9 @@ class GUI_EXPORT QgsFileDownloader : public QObject
* \param url the download url
* \param outputFileName file name where the downloaded content will be stored
* \param guiNotificationsEnabled if false, the downloader will not display any progress bar or error message
* \param authcfg optionally apply this authentication configuration
*/
QgsFileDownloader( const QUrl &url, const QString &outputFileName, bool guiNotificationsEnabled = true );
QgsFileDownloader( const QUrl &url, const QString &outputFileName, bool guiNotificationsEnabled = true, QString authcfg = QString() );

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

public slots:
@@ -114,6 +116,7 @@ class GUI_EXPORT QgsFileDownloader : public QObject
bool mDownloadCanceled;
QStringList mErrors;
bool mGuiNotificationsEnabled;
QString mAuthCfg;
};

#endif // QGSFILEDOWNLOADER_H
@@ -8,7 +8,8 @@
configuration to access an HTTP Basic protected endpoint.
From build dir, run: ctest -R PyQgsAuthManagerPasswordOWSTest -V
From build dir, run from test directory:
LC_ALL=EN ctest -R PyQgsAuthManagerPasswordOWSTest -V
.. note:: 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
@@ -23,6 +24,7 @@
import random
import string
import urllib
from functools import partial

__author__ = 'Alessandro Pasotti'
__date__ = '18/09/2016'
@@ -39,10 +41,17 @@
QgsVectorLayer,
QgsRasterLayer,
)
from qgis.gui import (
QgsFileDownloader,
)
from qgis.testing import (
start_app,
unittest,
)
from qgis.PyQt.QtCore import (
QEventLoop,
QUrl,
)

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

def testInvalidAuthFileDownload(self):
"""
Download a protected map tile without authcfg
"""
qs = "?" + "&".join(["%s=%s" % i for i in list({
"MAP": urllib.parse.quote(self.project_path),
"SERVICE": "WMS",
"VERSION": "1.1.1",
"REQUEST": "GetMap",
"LAYERS": "testlayer_èé".replace('_', '%20'),
"STYLES": "",
"FORMAT": "image/png",
"BBOX": "-16817707,-4710778,5696513,14587125",
"HEIGHT": "500",
"WIDTH": "500",
"CRS": "EPSG:3857"
}.items())])
url = '%s://%s:%s/%s' % (self.protocol, self.hostname, self.port, qs)

destination = tempfile.mktemp()
loop = QEventLoop()

downloader = QgsFileDownloader(QUrl(url), destination, False)
downloader.downloadCompleted.connect(partial(self._set_slot, 'completed'))
downloader.downloadExited.connect(partial(self._set_slot, 'exited'))
downloader.downloadCanceled.connect(partial(self._set_slot, 'canceled'))
downloader.downloadError.connect(partial(self._set_slot, 'error'))
downloader.downloadProgress.connect(partial(self._set_slot, 'progress'))

downloader.downloadExited.connect(loop.quit)

loop.exec_()

self.assertTrue(self.error_was_called)
self.assertTrue("Download failed: Host requires authentication" in str(self.error_args), "Error args is: %s" % str(self.error_args))

def testValidAuthFileDownload(self):
"""
Download a map tile with valid authcfg
"""
qs = "?" + "&".join(["%s=%s" % i for i in list({
"MAP": urllib.parse.quote(self.project_path),
"SERVICE": "WMS",
"VERSION": "1.1.1",
"REQUEST": "GetMap",
"LAYERS": "testlayer_èé".replace('_', '%20'),
"STYLES": "",
"FORMAT": "image/png",
"BBOX": "-16817707,-4710778,5696513,14587125",
"HEIGHT": "500",
"WIDTH": "500",
"CRS": "EPSG:3857"
}.items())])
url = '%s://%s:%s/%s' % (self.protocol, self.hostname, self.port, qs)

destination = tempfile.mktemp()
loop = QEventLoop()

downloader = QgsFileDownloader(QUrl(url), destination, False, self.auth_config.id())
downloader.downloadCompleted.connect(partial(self._set_slot, 'completed'))
downloader.downloadExited.connect(partial(self._set_slot, 'exited'))
downloader.downloadCanceled.connect(partial(self._set_slot, 'canceled'))
downloader.downloadError.connect(partial(self._set_slot, 'error'))
downloader.downloadProgress.connect(partial(self._set_slot, 'progress'))

downloader.downloadExited.connect(loop.quit)

loop.exec_()

# Check the we've got a likely PNG image
self.assertTrue(self.completed_was_called)
self.assertTrue(os.path.getsize(destination) > 700000, "Image size: %s" % os.path.getsize(destination)) # > 1MB
with open(destination, 'rb') as f:
self.assertTrue(b'PNG' in f.read()) # is a PNG

def _set_slot(self, *args, **kwargs):
#print('_set_slot(%s) called' % args[0])
setattr(self, args[0] + '_was_called', True)
setattr(self, args[0] + '_args', args)


if __name__ == '__main__':
unittest.main()
@@ -2,6 +2,9 @@
"""
Test the QgsFileDownloader class
Run test with:
LC_ALL=EN ctest -V -R PyQgsFileDownloader
.. note:: 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

0 comments on commit 66f4ff9

Please sign in to comment.