Skip to content

Commit

Permalink
Merge pull request #32694 from elpaso/server-wfs3-transaction-simple-2
Browse files Browse the repository at this point in the history
[feature] Server OAPIF simple transactions
  • Loading branch information
elpaso committed Nov 15, 2019
2 parents ebc9888 + 61fdbf7 commit 2eb4108
Show file tree
Hide file tree
Showing 30 changed files with 4,354 additions and 367 deletions.
9 changes: 3 additions & 6 deletions python/server/auto_generated/qgsserverapiutils.sip.in
Expand Up @@ -103,18 +103,15 @@ intervals describes the temporal extent.
Parses the CRS URI ``bboxCrs`` (example: "http://www.opengis.net/def/crs/OGC/1.3/CRS84") into a QGIS CRS object
%End

static const QVector<QgsMapLayer *> publishedWfsLayers( const QgsProject *project );
static const QVector<QgsVectorLayer *> publishedWfsLayers( const QgsServerApiContext &context );
%Docstring
Returns the list of layers accessible to the service for a given ``project``.
Returns the list of layers accessible to the service for a given ``context``.

This method takes into account the ACL restrictions provided by QGIS Server Access Control plugins.

.. note::

project must not be NULL
%End



static QString sanitizedFieldValue( const QString &value );
%Docstring
Sanitizes the input ``value`` by removing URL encoding and checking for malicious content.
Expand Down
14 changes: 13 additions & 1 deletion python/server/auto_generated/qgsserverrequest.sip.in
Expand Up @@ -16,6 +16,9 @@ class QgsServerRequest
%TypeHeaderCode
#include "qgsserverrequest.h"
%End
public:
static const QMetaObject staticMetaObject;

public:

typedef QMap<QString, QString> Parameters;
Expand All @@ -27,7 +30,8 @@ class QgsServerRequest
PutMethod,
GetMethod,
PostMethod,
DeleteMethod
DeleteMethod,
PatchMethod
};


Expand Down Expand Up @@ -56,6 +60,14 @@ Constructor

virtual ~QgsServerRequest();

static QString methodToString( const Method &method );
%Docstring
Returns a string representation of an HTTP request ``method``.

.. versionadded:: 3.12
%End


QUrl url() const;
%Docstring

Expand Down
20 changes: 20 additions & 0 deletions resources/server/api/ogc/static/style.css
Expand Up @@ -53,3 +53,23 @@ table.table-params td.small
{
font-size: 90%;
}

.btn-post, .btn-post:hover {
background-color: green;
border-color: green;
}

.btn-put, btn-put:hover {
background-color: orange;
border-color: orange;
}

.btn-patch, .btn-patch:hover {
background-color: magenta;
border-color: magenta;
}

.btn-delete, .btn-delete:hover {
background-color: red;
border-color: red;
}
Expand Up @@ -27,6 +27,15 @@ <h3>Extent</h3>
<dt>{{ extent.spatial.bbox.0.3 }}</dt>
</dl>

<h3>Links</h3>
<ul>
{% for link in links %}
{% if link.rel != "self" %}
<li><a rel="{{ link.rel }}" href="{{ link.href }}">{{ link.title }}</a></li>
{% endif %}
{% endfor %}
</ul>

</div>

<div class="col-md-6">
Expand Down
Expand Up @@ -27,7 +27,7 @@ <h2>Paths</h2>
{% for method, method_data in path_info %}
<div class="card-header" id="heading_{{ method_data.operationId }}">
<h5 class="mb-0">
<span class="path-button btn btn-primary" data-toggle="collapse"
<span class="path-button btn btn-primary btn-{{ method }}" data-toggle="collapse"
data-target="#collapse_{{ method_data.operationId }}"
aria-expanded="true"
aria-controls="collapse_{{ method_data.operationId }}">{{ method }}</span>
Expand Down
4 changes: 3 additions & 1 deletion resources/server/api/ogc/templates/wfs3/header.html
Expand Up @@ -14,9 +14,11 @@
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
crossorigin="anonymous"></script>
<link rel="stylesheet" href="{{ static( "jsonFormatter.min.css" ) }}" crossorigin="anonymous"></script>
<link rel="stylesheet" href="{{ static( "jsonFormatter.min.css" ) }}" crossorigin="anonymous">
<script src="{{ static( "jsonFormatter.min.js" ) }}" crossorigin="anonymous"></script>

{% include "links.html" %}

<title>{{ metadata.pageTitle }}</title>
</head>
<body>
Expand Down
6 changes: 2 additions & 4 deletions resources/server/api/ogc/templates/wfs3/links.html
@@ -1,6 +1,4 @@
<!-- template for the WFS3 API links list -->
<ul>
<!-- template for the WFS3 API links list, to be included in HEAD -->
{% for link in links %}
<li><a rel="{{ link.rel }}" href="{{ link.href }}">{{ link.title }}</a></li>
<link rel="{{ link.rel }}" href="{{ link.href }}" title="{{ link.title }}" type="{{ link.type }}">
{% endfor %}
</ul>
3 changes: 3 additions & 0 deletions src/core/qgsjsonutils.cpp
Expand Up @@ -436,6 +436,9 @@ json QgsJsonUtils::jsonFromVariant( const QVariant &val )
case QMetaType::Bool:
j = val.toBool();
break;
case QMetaType::QByteArray:
j = val.toByteArray().toBase64().toStdString();
break;
default:
j = val.toString().toStdString();
break;
Expand Down
4 changes: 4 additions & 0 deletions src/server/qgsfcgiserverrequest.cpp
Expand Up @@ -106,6 +106,10 @@ QgsFcgiServerRequest::QgsFcgiServerRequest()
{
method = HeadMethod;
}
else if ( strcmp( me, "PATCH" ) == 0 )
{
method = PatchMethod;
}
}

if ( method == PostMethod || method == PutMethod )
Expand Down
107 changes: 57 additions & 50 deletions src/server/qgsrequesthandler.cpp
Expand Up @@ -189,70 +189,77 @@ void QgsRequestHandler::setupParameters()

void QgsRequestHandler::parseInput()
{
if ( mRequest.method() == QgsServerRequest::PostMethod )
if ( mRequest.method() == QgsServerRequest::PostMethod ||
mRequest.method() == QgsServerRequest::PutMethod ||
mRequest.method() == QgsServerRequest::PatchMethod )
{
QString inputString( mRequest.data() );

QDomDocument doc;
QString errorMsg;
int line = -1;
int column = -1;
if ( !doc.setContent( inputString, true, &errorMsg, &line, &column ) )
if ( mRequest.header( QStringLiteral( "Content-Type" ) ).contains( QStringLiteral( "json" ) ) )
{
// XXX Output error but continue processing request ?
QgsMessageLog::logMessage( QStringLiteral( "Warning: error parsing post data as XML: at line %1, column %2: %3. Assuming urlencoded query string sent in the post body." )
.arg( line ).arg( column ).arg( errorMsg ) );

// Process input string as a simple query text

typedef QPair<QString, QString> pair_t;
QUrlQuery query( inputString );
QList<pair_t> items = query.queryItems();
for ( pair_t pair : items )
{
// QUrl::fromPercentEncoding doesn't replace '+' with space
const QString key = QUrl::fromPercentEncoding( pair.first.replace( '+', ' ' ).toUtf8() );
const QString value = QUrl::fromPercentEncoding( pair.second.replace( '+', ' ' ).toUtf8() );
mRequest.setParameter( key.toUpper(), value );
}
setupParameters();
}
else
{
// we have an XML document

setupParameters();

QDomElement docElem = doc.documentElement();
// the document element tag name is the request
mRequest.setParameter( QStringLiteral( "REQUEST" ), docElem.tagName() );
// loop through the attributes which are the parameters
// excepting the attributes started by xmlns or xsi
QDomNamedNodeMap map = docElem.attributes();
for ( int i = 0 ; i < map.length() ; ++i )
QString inputString( mRequest.data() );
QDomDocument doc;
QString errorMsg;
int line = -1;
int column = -1;
if ( !doc.setContent( inputString, true, &errorMsg, &line, &column ) )
{
if ( map.item( i ).isNull() )
continue;

const QDomNode attrNode = map.item( i );
const QDomAttr attr = attrNode.toAttr();
if ( attr.isNull() )
continue;

const QString attrName = attr.name();
if ( attrName.startsWith( "xmlns" ) || attrName.startsWith( "xsi:" ) )
continue;

mRequest.setParameter( attrName.toUpper(), attr.value() );
// XXX Output error but continue processing request ?
QgsMessageLog::logMessage( QStringLiteral( "Warning: error parsing post data as XML: at line %1, column %2: %3. Assuming urlencoded query string sent in the post body." )
.arg( line ).arg( column ).arg( errorMsg ) );

// Process input string as a simple query text

typedef QPair<QString, QString> pair_t;
QUrlQuery query( inputString );
const QList<pair_t> items = query.queryItems();
for ( const pair_t &pair : items )
{
// QUrl::fromPercentEncoding doesn't replace '+' with space
const QString key = QUrl::fromPercentEncoding( QString( pair.first ).replace( '+', ' ' ).toUtf8() );
const QString value = QUrl::fromPercentEncoding( QString( pair.second ).replace( '+', ' ' ).toUtf8() );
mRequest.setParameter( key.toUpper(), value );
}
setupParameters();
}
else
{
// we have an XML document

setupParameters();

QDomElement docElem = doc.documentElement();
// the document element tag name is the request
mRequest.setParameter( QStringLiteral( "REQUEST" ), docElem.tagName() );
// loop through the attributes which are the parameters
// excepting the attributes started by xmlns or xsi
QDomNamedNodeMap map = docElem.attributes();
for ( int i = 0 ; i < map.length() ; ++i )
{
if ( map.item( i ).isNull() )
continue;

const QDomNode attrNode = map.item( i );
const QDomAttr attr = attrNode.toAttr();
if ( attr.isNull() )
continue;

const QString attrName = attr.name();
if ( attrName.startsWith( "xmlns" ) || attrName.startsWith( "xsi:" ) )
continue;

mRequest.setParameter( attrName.toUpper(), attr.value() );
}
mRequest.setParameter( QStringLiteral( "REQUEST_BODY" ), inputString );
}
mRequest.setParameter( QStringLiteral( "REQUEST_BODY" ), inputString );
}
}
else
{
setupParameters();
}

}

void QgsRequestHandler::setParameter( const QString &key, const QString &value )
Expand Down
20 changes: 2 additions & 18 deletions src/server/qgsserverapiutils.cpp
Expand Up @@ -563,25 +563,9 @@ QgsCoordinateReferenceSystem QgsServerApiUtils::parseCrs( const QString &bboxCrs
}
}

const QVector<QgsMapLayer *> QgsServerApiUtils::publishedWfsLayers( const QgsProject *project )
const QVector<QgsVectorLayer *> QgsServerApiUtils::publishedWfsLayers( const QgsServerApiContext &context )
{
const QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project );
const QStringList wfstUpdateLayersId = QgsServerProjectUtils::wfstUpdateLayerIds( *project );
const QStringList wfstInsertLayersId = QgsServerProjectUtils::wfstInsertLayerIds( *project );
const QStringList wfstDeleteLayersId = QgsServerProjectUtils::wfstDeleteLayerIds( *project );
QVector<QgsMapLayer *> result;
const auto constLayers { project->mapLayers() };
for ( auto it = project->mapLayers().constBegin(); it != project->mapLayers().constEnd(); it++ )
{
if ( wfstUpdateLayersId.contains( it.value()->id() ) ||
wfstInsertLayersId.contains( it.value()->id() ) ||
wfstDeleteLayersId.contains( it.value()->id() ) )
{
result.push_back( it.value() );
}

}
return result;
return publishedWfsLayers< QgsVectorLayer * >( context );
}

QString QgsServerApiUtils::sanitizedFieldValue( const QString &value )
Expand Down
15 changes: 7 additions & 8 deletions src/server/qgsserverapiutils.h
Expand Up @@ -152,13 +152,11 @@ class SERVER_EXPORT QgsServerApiUtils
static QgsCoordinateReferenceSystem parseCrs( const QString &bboxCrs );

/**
* Returns the list of layers accessible to the service for a given \a project.
* Returns the list of layers accessible to the service for a given \a context.
*
* This method takes into account the ACL restrictions provided by QGIS Server Access Control plugins.
*
* \note project must not be NULL
*/
static const QVector<QgsMapLayer *> publishedWfsLayers( const QgsProject *project );
static const QVector<QgsVectorLayer *> publishedWfsLayers( const QgsServerApiContext &context );

#ifndef SIP_RUN

Expand All @@ -167,22 +165,22 @@ class SERVER_EXPORT QgsServerApiUtils
*
* Example:
*
* QVector<QgsVectorLayer*> vectorLayers = publishedLayers<QgsVectorLayer>();
* QVector<const QgsVectorLayer*> vectorLayers = publishedLayers<QgsVectorLayer>();
*
* \note not available in Python bindings
*/
template <typename T>
static const QVector<const T *> publishedWfsLayers( const QgsServerApiContext &context )
static const QVector<T> publishedWfsLayers( const QgsServerApiContext &context )
{
#ifdef HAVE_SERVER_PYTHON_PLUGINS
QgsAccessControl *accessControl = context.serverInterface()->accessControls();
#endif
const QgsProject *project = context.project();
QVector<const T *> result;
QVector<T> result;
if ( project )
{
const QStringList wfsLayerIds = QgsServerProjectUtils::wfsLayerIds( *project );
const auto constLayers { project->layers<T *>() };
const auto constLayers { project->layers<T>() };
for ( const auto &layer : constLayers )
{
if ( ! wfsLayerIds.contains( layer->id() ) )
Expand All @@ -203,6 +201,7 @@ class SERVER_EXPORT QgsServerApiUtils

#endif


/**
* Sanitizes the input \a value by removing URL encoding and checking for malicious content.
* In case of failure returns an empty string.
Expand Down
21 changes: 21 additions & 0 deletions src/server/qgsserverexception.h
Expand Up @@ -251,6 +251,27 @@ class SERVER_EXPORT QgsServerApiBadRequestException: public QgsServerApiExceptio
}
};


/**
* \ingroup server
* \class QgsServerApiPermissionDeniedException
* \brief Forbidden (permission denied) 403
*
* Note that this exception is associated with a default return code 403 which may be
* not appropriate in some situations.
*
* \since QGIS 3.12
*/
class SERVER_EXPORT QgsServerApiPermissionDeniedException: public QgsServerApiException
{
public:
//! Construction
QgsServerApiPermissionDeniedException( const QString &message, const QString &mimeType = QStringLiteral( "application/json" ), int responseCode = 403 )
: QgsServerApiException( QStringLiteral( "Forbidden" ), message, mimeType, responseCode )
{
}
};

/**
* \ingroup server
* \class QgsServerApiImproperlyConfiguredException
Expand Down

0 comments on commit 2eb4108

Please sign in to comment.