Skip to content

Commit

Permalink
Merge pull request #4407 from boundlessgeo/filedownloader-auth
Browse files Browse the repository at this point in the history
[auth] Add authentication configuration support to QgsFileDownloader
  • Loading branch information
dakcarto authored Apr 25, 2017
2 parents c869fa2 + ae7ace9 commit 66f4ff9
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 65 deletions.
1 change: 0 additions & 1 deletion python/auto_sip.blacklist
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
139 changes: 79 additions & 60 deletions python/gui/qgsfiledownloader.sip
Original file line number Diff line number Diff line change
@@ -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 *
************************************************************************/
15 changes: 14 additions & 1 deletion src/gui/qgsfiledownloader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include "qgsfiledownloader.h"
#include "qgsnetworkaccessmanager.h"
#include "qgsapplication.h"
#include "qgsauthmanager.h"

#include <QNetworkAccessManager>
#include <QNetworkRequest>
Expand All @@ -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();
}

Expand All @@ -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 );
Expand All @@ -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 );
Expand Down
7 changes: 5 additions & 2 deletions src/gui/qgsfiledownloader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -114,6 +116,7 @@ class GUI_EXPORT QgsFileDownloader : public QObject
bool mDownloadCanceled;
QStringList mErrors;
bool mGuiNotificationsEnabled;
QString mAuthCfg;
};

#endif // QGSFILEDOWNLOADER_H
91 changes: 90 additions & 1 deletion tests/src/python/test_authmanager_password_ows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,6 +24,7 @@
import random
import string
import urllib
from functools import partial

__author__ = 'Alessandro Pasotti'
__date__ = '18/09/2016'
Expand All @@ -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']
Expand Down Expand Up @@ -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()
3 changes: 3 additions & 0 deletions tests/src/python/test_qgsfiledownloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 66f4ff9

Please sign in to comment.