[auth] Add authentication configuration support to QgsFileDownloader

With tests.
This commit is contained in:
Alessandro Pasotti 2017-04-24 17:30:23 +02:00
parent d3453063ea
commit e5969452df
6 changed files with 190 additions and 65 deletions

View File

@ -353,7 +353,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

View File

@ -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
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();
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
private:
~QgsFileDownloader();
public slots:
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 *
************************************************************************/

View File

@ -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 );

View File

@ -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

View File

@ -8,7 +8,8 @@ and QGIS Server WFS/WMS that check if QGIS can use a stored auth manager auth
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 tempfile
import random
import string
import urllib
from functools import partial
__author__ = 'Alessandro Pasotti'
__date__ = '18/09/2016'
@ -39,10 +41,17 @@ from qgis.core import (
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,85 @@ class TestAuthManager(unittest.TestCase):
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()

View File

@ -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