Server landing page plugin

This commit is contained in:
Alessandro Pasotti 2020-08-03 19:36:20 +02:00
parent 47c7862b66
commit 13a5bc4459
25 changed files with 998 additions and 2 deletions

View File

@ -0,0 +1 @@
.v-app-bar.v-app-bar--fixed,.v-footer{z-index:10000!important}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
dt[data-v-1d7961f2]{font-weight:700}.leaflet-container[data-v-3630fe7c]{height:20rem}.card-footer .btn[data-v-3630fe7c]{margin-right:.5em}h4.loading[data-v-3630fe7c]{margin-top:.35em}.metadata[data-v-3630fe7c]{z-index:1001}.v-dialog{border-radius:4px;margin:24px;overflow-y:auto;pointer-events:auto;transition:.3s cubic-bezier(.25,.8,.25,1);width:100%;z-index:inherit;box-shadow:0 11px 15px -7px rgba(0,0,0,.2),0 24px 38px 3px rgba(0,0,0,.14),0 9px 46px 8px rgba(0,0,0,.12)}.v-dialog:not(.v-dialog--fullscreen){max-height:90%}.v-dialog>*{width:100%}.v-dialog>.v-card>.v-card__title{font-size:1.25rem;font-weight:500;letter-spacing:.0125em;padding:16px 24px 10px}.v-dialog>.v-card>.v-card__subtitle,.v-dialog>.v-card>.v-card__text{padding:0 24px 20px}.v-dialog__content{align-items:center;display:flex;height:100%;justify-content:center;left:0;pointer-events:none;position:fixed;top:0;transition:.2s cubic-bezier(.25,.8,.25,1),z-index 1ms;width:100%;z-index:6;outline:none}.v-dialog__container{display:none}.v-dialog__container--attached{display:inline}.v-dialog--animated{-webkit-animation-duration:.15s;animation-duration:.15s;-webkit-animation-name:animate-dialog;animation-name:animate-dialog;-webkit-animation-timing-function:cubic-bezier(.25,.8,.25,1);animation-timing-function:cubic-bezier(.25,.8,.25,1)}.v-dialog--fullscreen{border-radius:0;margin:0;height:100%;position:fixed;overflow-y:auto;top:0;left:0}.v-dialog--fullscreen>.v-card{min-height:100%;min-width:100%;margin:0!important;padding:0!important}.v-dialog--scrollable,.v-dialog--scrollable>form{display:flex}.v-dialog--scrollable>.v-card,.v-dialog--scrollable>form>.v-card{display:flex;flex:1 1 100%;flex-direction:column;max-height:100%;max-width:100%}.v-dialog--scrollable>.v-card>.v-card__actions,.v-dialog--scrollable>.v-card>.v-card__title,.v-dialog--scrollable>form>.v-card>.v-card__actions,.v-dialog--scrollable>form>.v-card>.v-card__title{flex:0 0 auto}.v-dialog--scrollable>.v-card>.v-card__text,.v-dialog--scrollable>form>.v-card>.v-card__text{-webkit-backface-visibility:hidden;backface-visibility:hidden;flex:1 1 auto;overflow-y:auto}@-webkit-keyframes animate-dialog{0%{transform:scale(1)}50%{transform:scale(1.03)}to{transform:scale(1)}}@keyframes animate-dialog{0%{transform:scale(1)}50%{transform:scale(1.03)}to{transform:scale(1)}}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge"><meta name=viewport content="width=device-width,initial-scale=1,shrink-to-fit=no"><link rel=icon href=/favicon.ico><title>app</title><link rel=stylesheet href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"><link rel=stylesheet href=https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css><link href=/css/chunk-17be74ed.d8e3c767.css rel=prefetch><link href=/css/chunk-78671b57.a2bc3c6b.css rel=prefetch><link href=/css/chunk-a84730f4.d8136b79.css rel=prefetch><link href=/js/chunk-17be74ed.28f97f46.js rel=prefetch><link href=/js/chunk-78671b57.5200390a.js rel=prefetch><link href=/js/chunk-a84730f4.db1de6e9.js rel=prefetch><link href=/css/app.ca3f5643.css rel=preload as=style><link href=/css/chunk-vendors.4f60a56b.css rel=preload as=style><link href=/js/app.c7284a81.js rel=preload as=script><link href=/js/chunk-vendors.a274ab4b.js rel=preload as=script><link href=/css/chunk-vendors.4f60a56b.css rel=stylesheet><link href=/css/app.ca3f5643.css rel=stylesheet></head><body><noscript><strong>We're sorry but app doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script src=/js/chunk-vendors.a274ab4b.js></script><script src=/js/app.c7284a81.js></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,55 @@
########################################################
# Files
SET (LANDINGPAGE_SRCS
${CMAKE_SOURCE_DIR}/external/nlohmann/json.hpp
${CMAKE_SOURCE_DIR}/external/inja/inja.hpp
qgslandingpage.cpp
qgslandingpageutils.cpp
qgslandingpagehandlers.cpp
)
########################################################
# Build
ADD_LIBRARY (landingpage MODULE ${LANDINGPAGE_SRCS})
INCLUDE_DIRECTORIES(SYSTEM
)
INCLUDE_DIRECTORIES(
${CMAKE_SOURCE_DIR}/external
${CMAKE_SOURCE_DIR}/external/nlohmann
${CMAKE_SOURCE_DIR}/src/core
${CMAKE_SOURCE_DIR}/src/core/geometry
${CMAKE_SOURCE_DIR}/src/core/expression
${CMAKE_SOURCE_DIR}/src/core/symbology
${CMAKE_SOURCE_DIR}/src/core/metadata
${CMAKE_SOURCE_DIR}/src/core/layertree
${CMAKE_SOURCE_DIR}/src/server
${CMAKE_SOURCE_DIR}/src/server/services
${CMAKE_SOURCE_DIR}/src/server/services/landingpage
${CMAKE_BINARY_DIR}/src/core
${CMAKE_BINARY_DIR}/src/python
${CMAKE_BINARY_DIR}/src/server
${CMAKE_CURRENT_BINARY_DIR}
)
TARGET_LINK_LIBRARIES(landingpage
qgis_core
qgis_server
)
########################################################
# Install
INSTALL(TARGETS landingpage
RUNTIME DESTINATION ${QGIS_SERVER_MODULE_DIR}
LIBRARY DESTINATION ${QGIS_SERVER_MODULE_DIR}
)

View File

@ -0,0 +1,133 @@
/***************************************************************************
qgslandingpage.cpp
-------------------------
begin : August 3, 2020
copyright : (C) 2020 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. *
* *
***************************************************************************/
#include "qgsmodule.h"
#include "qgsserverogcapi.h"
#include "qgsserverfilter.h"
#include "qgslandingpagehandlers.h"
#include "qgslandingpageutils.h"
#include "qgsserverstatichandler.h"
#include "qgsmessagelog.h"
/**
* Landing page API
* \since QGIS 3.16
*/
class QgsLandingPageApi: public QgsServerOgcApi
{
public:
QgsLandingPageApi( QgsServerInterface *serverIface,
const QString &rootPath,
const QString &name,
const QString &description = QString(),
const QString &version = QString() )
: QgsServerOgcApi( serverIface, rootPath, name, description, version )
{
}
bool accept( const QUrl &url ) const override
{
// Mainly for CI testing of legacy OGC XML responses, we offer a way to disable landingpage API.
// The plugin installation is optional so this won't be an issue in production.
return ! qgetenv( "QGIS_SERVER_DISABLED_APIS" ).contains( name().toUtf8() ) && ( url.path().isEmpty()
|| url.path() == '/'
|| url.path().startsWith( QStringLiteral( "/map/" ) )
|| url.path().startsWith( QStringLiteral( "/index" ) )
// Statics:
|| url.path().startsWith( QStringLiteral( "/css/" ) )
|| url.path().startsWith( QStringLiteral( "/js/" ) )
|| url.path().startsWith( QStringLiteral( "/public/" ) ) );
}
};
/**
* Sets QGIS_PROJECT_FILE from /project/<hash>/ URL fragment
* \since QGIS 3.16
*/
class QgsProjectLoaderFilter: public QgsServerFilter
{
// QgsServerFilter interface
public:
QgsProjectLoaderFilter( QgsServerInterface *serverIface )
: QgsServerFilter( serverIface )
{
}
void requestReady() override
{
const auto handler { serverInterface()->requestHandler() };
if ( handler->path().startsWith( QStringLiteral( "/project/" ) ) )
{
const QString projectPath { QgsLandingPageUtils::projectPathFromUrl( handler->url() ) };
if ( ! projectPath.isEmpty() )
{
qputenv( "QGIS_PROJECT_FILE", projectPath.toUtf8() );
serverInterface()->setConfigFilePath( projectPath.toUtf8() );
QgsMessageLog::logMessage( QStringLiteral( "Project from URL set to: %1" ).arg( projectPath ), QStringLiteral( "Landing Page Plugin" ), Qgis::MessageLevel::Info );
}
else
{
QgsMessageLog::logMessage( QStringLiteral( "Could not get project from URL: %1" ).arg( handler->url() ), QStringLiteral( "Landing Page Plugin" ), Qgis::MessageLevel::Info );
}
}
};
};
/**
* \class QgsLandingPageModule
* \brief Landing page module for QGIS Server
* \since QGIS 3.16
*/
class QgsLandingPageModule: public QgsServiceModule
{
public:
void registerSelf( QgsServiceRegistry &registry, QgsServerInterface *serverIface ) override
{
QgsLandingPageApi *landingPageApi = new QgsLandingPageApi{ serverIface,
QStringLiteral( "/" ),
QStringLiteral( "Landing Page" ),
QStringLiteral( "1.0.0" )
};
// Register handlers
landingPageApi->registerHandler<QgsServerStaticHandler>( QStringLiteral( "/(?<staticFilePath>(public|css|js)/.*)$" ), QStringLiteral( "landingpage" ) );
landingPageApi->registerHandler<QgsLandingPageHandler>();
landingPageApi->registerHandler<QgsLandingPageMapHandler>();
// Register API
registry.registerApi( landingPageApi );
// Register filters
serverIface->registerFilter( new QgsProjectLoaderFilter( serverIface ) );
}
};
// Entry points
QGISEXTERN QgsServiceModule *QGS_ServiceModule_Init()
{
static QgsLandingPageModule module;
return &module;
}
QGISEXTERN void QGS_ServiceModule_Exit( QgsServiceModule * )
{
// Nothing to do
}

View File

@ -0,0 +1,97 @@
/***************************************************************************
qgsLandingPagehandlers.cpp
-------------------------
begin : May 3, 2019
copyright : (C) 2019 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. *
* *
***************************************************************************/
#include "qgslandingpagehandlers.h"
#include "qgslandingpageutils.h"
#include "qgsserverinterface.h"
#include "qgsserverresponse.h"
#include "qgsproject.h"
#include "qgsserverprojectutils.h"
#include "qgsvectorlayer.h"
#include "qgslayertreenode.h"
#include "qgslayertree.h"
#include <QDir>
#include <QCryptographicHash>
QgsLandingPageHandler::QgsLandingPageHandler()
{
setContentTypes( { QgsServerOgcApi::ContentType::JSON, QgsServerOgcApi::ContentType::HTML } );
}
void QgsLandingPageHandler::handleRequest( const QgsServerApiContext &context ) const
{
if ( context.request()->url().path( ) == '/' || context.request()->url().path( ).isEmpty() )
{
QUrl url { context.request()->url() };
url.setPath( QStringLiteral( "/index.%1" ).arg( QgsServerOgcApi::contentTypeToExtension( contentTypeFromRequest( context.request() ) ) ) );
context.response()->setStatusCode( 302 );
context.response()->setHeader( QStringLiteral( "Location" ), url.toString() );
}
else
{
const json projects { projectsData( ) };
json data
{
{ "links", links( context ) },
{ "projects", projects },
{ "projects_count", projects.size() }
};
write( data, context, {{ "pageTitle", linkTitle() }, { "navigation", json::array() }} );
}
}
const QString QgsLandingPageHandler::templatePath( const QgsServerApiContext &context ) const
{
QString path { context.serverInterface()->serverSettings()->apiResourcesDirectory() };
path += QStringLiteral( "/ogc/static/landingpage/index.html" );
return path;
}
json QgsLandingPageHandler::projectsData() const
{
json j { json::array() };
const auto availableProjects { QgsLandingPageUtils::projects( ) };
const auto constProjectKeys { availableProjects.keys() };
for ( const auto &p : constProjectKeys )
{
auto info { QgsLandingPageUtils::projectInfo( availableProjects[ p ] ) };
info[ "id" ] = p.toStdString();
j.push_back( info );
}
return j;
}
QgsLandingPageMapHandler::QgsLandingPageMapHandler()
{
setContentTypes( { QgsServerOgcApi::ContentType::JSON } );
}
void QgsLandingPageMapHandler::handleRequest( const QgsServerApiContext &context ) const
{
json data;
data[ "links" ] = json::array();
const QString projectPath { QgsLandingPageUtils::projectPathFromUrl( context.request()->url().path() ) };
if ( projectPath.isEmpty() )
{
throw QgsServerApiNotFoundError( QStringLiteral( "Requested project hash not found!" ) );
}
data[ "project" ] = QgsLandingPageUtils::projectInfo( projectPath );
write( data, context, {{ "pageTitle", linkTitle() }, { "navigation", json::array() }} );
}

View File

@ -0,0 +1,91 @@
/***************************************************************************
qgslandingpagehandlers.h
-------------------------
begin : July 30, 2020
copyright : (C) 2020 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. *
* *
***************************************************************************/
#ifndef QGS_LANDINGPAGE_HANDLERS_H
#define QGS_LANDINGPAGE_HANDLERS_H
#include "qgsserverogcapihandler.h"
#include "qgsfields.h"
class QgsFeatureRequest;
class QgsServerOgcApi;
class QgsFeature;
/**
* The QgsLandingPageHandler implements the landing page handler.
*/
class QgsLandingPageHandler: public QgsServerOgcApiHandler
{
public:
QgsLandingPageHandler( );
void handleRequest( const QgsServerApiContext &context ) const override;
// QgsServerOgcApiHandler interface
QRegularExpression path() const override { return QRegularExpression( R"re(^/(index.html|index.json)?$)re" ); }
std::string operationId() const override { return "getLandingPage"; }
QStringList tags() const override { return { QStringLiteral( "Catalog" ) }; }
std::string summary() const override
{
return "Server Landing Page";
}
std::string description() const override
{
return "The landing page provides information about available projects and services.";
}
std::string linkTitle() const override { return "Landing page"; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::self; }
const QString templatePath( const QgsServerApiContext &context ) const override;
private:
json projectsData() const;
};
/**
* The QgsLandingPageMapHandler implements the landing page map handler (JSON only).
*/
class QgsLandingPageMapHandler: public QgsServerOgcApiHandler
{
public:
QgsLandingPageMapHandler( );
void handleRequest( const QgsServerApiContext &context ) const override;
// QgsServerOgcApiHandler interface
QRegularExpression path() const override { return QRegularExpression( R"re(^/map/([a-f0-9]{32}).*$)re" ); }
std::string operationId() const override { return "getMap"; }
QStringList tags() const override { return { QStringLiteral( "Catalog" ), QStringLiteral( "Map Viewer" ) }; }
std::string summary() const override
{
return "Server Map Viewer";
}
std::string description() const override
{
return "Shows a map";
}
std::string linkTitle() const override { return "Map Viewer"; }
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::self; }
};
#endif // QGS_LANDINGPAGE_HANDLERS_H

View File

@ -0,0 +1,486 @@
/***************************************************************************
qgslandingpageutils.cpp - QgsLandingPageUtils
---------------------
begin : 3.8.2020
copyright : (C) 2020 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. *
* *
***************************************************************************/
#include "qgslandingpageutils.h"
#include "qgsserverprojectutils.h"
#include "qgsmessagelog.h"
#include "qgslayertree.h"
#include "qgsvectorlayer.h"
#include "nlohmann/json.hpp"
#include <QCryptographicHash>
const QRegularExpression QgsLandingPageUtils::PROJECT_HASH_RE { QStringLiteral( "/(?<projectHash>[a-f0-9]{32})" ) };
QMap<QString, QString> QgsLandingPageUtils::projects( )
{
// TODO: cache this information and use a dir-watcher to invalidate
QMap<QString, QString> availableProjects;
for ( const auto &path : QString( qgetenv( "QGIS_SERVER_PROJECTS_DIRECTORIES" ) ).split( QStringLiteral( "||" ) ) )
{
const QDir dir { path };
if ( dir.exists() )
{
const auto constFiles { dir.entryList( ) };
for ( const auto &f : constFiles )
{
if ( f.endsWith( QStringLiteral( ".qgs" ), Qt::CaseSensitivity::CaseInsensitive ) ||
f.endsWith( QStringLiteral( ".qgz" ), Qt::CaseSensitivity::CaseInsensitive ) )
{
const QString fullPath { path + '/' + f };
availableProjects[ QCryptographicHash::hash( fullPath.toUtf8(), QCryptographicHash::Md5 ).toHex() ] = fullPath;
}
}
}
else
{
QgsMessageLog::logMessage( QStringLiteral( "QGIS_SERVER_PROJECTS_DIRECTORIES entry '%1' was not found: skipping." ).arg( path ), QStringLiteral( "Server" ), Qgis::MessageLevel::Warning );
}
}
// TODO: PG projects
return availableProjects;
}
json QgsLandingPageUtils::projectInfo( const QString &projectPath )
{
// Helper for QStringList
auto jList = [ ]( const QStringList & l ) -> json
{
json a = json::array( );
for ( const auto &e : qgis::as_const( l ) )
{
a.push_back( e.toStdString() );
}
return a;
};
auto jHash = [ ]( const QHash<QString, QString> &l ) -> json
{
json a;
const auto &constKeys { l.keys() };
for ( const auto &k : constKeys )
{
a[ k.toStdString() ] = l[ k ].toStdString();
}
return a;
};
auto jContactList = [ ]( const QgsAbstractMetadataBase::ContactList & contacts ) -> json
{
json jContacts = json::array();
for ( const auto &c : contacts )
{
json jContact
{
{ "name", c.name.toStdString() },
{ "role", c.role.toStdString() },
{ "voice", c.voice.toStdString() },
{ "fax", c.fax.toStdString() },
{ "email", c.email.toStdString() },
{ "position", c.position.toStdString() },
{ "organization", c.organization.toStdString() },
{ "addresses", json::array() }
};
// Addresses
const auto &addrs { c.addresses };
for ( const auto &a : addrs )
{
jContact[ "addresses" ].push_back(
{
{ "address", a.address.toStdString()},
{ "type", a.type.toStdString() },
{ "city", a.city.toStdString() },
{ "country", a.country.toStdString() },
{ "postalCode", a.postalCode.toStdString() },
{ "administrativeArea", a.administrativeArea.toStdString() }
} );
}
jContacts.push_back( jContacts );
}
return jContacts;
};
auto jLinksList = [ ]( const QgsAbstractMetadataBase::LinkList & links ) -> json
{
json jLinks = json::array();
for ( const auto &l : links )
{
jLinks.push_back(
{
{ "name", l.name.toStdString() },
{ "url", l.url.toStdString()},
{ "description", l.description.toStdString()},
{ "type", l.type.toStdString()},
{ "mimeType", l.mimeType.toStdString()},
{ "format", l.format.toStdString()}
} );
}
return jLinks;
};
json info;
QgsProject p;
if ( p.read( projectPath ) )
{
// Title
QString title { p.metadata().title() };
if ( title.isEmpty() )
title = QgsServerProjectUtils::owsServiceTitle( p );
if ( title.isEmpty() )
title = p.title();
if ( title.isEmpty() )
title = p.baseName();
info["title"] = title.toStdString();
// Description
QString description { p.metadata().abstract() };
if ( description.isEmpty() )
description = QgsServerProjectUtils::owsServiceAbstract( p );
info["description"] = description.toStdString();
// CRS
const QStringList wmsOutputCrsList { QgsServerProjectUtils::wmsOutputCrsList( p ) };
const QString crs { wmsOutputCrsList.contains( QStringLiteral( "EPSG:4326" ) ) || wmsOutputCrsList.isEmpty() ?
QStringLiteral( "EPSG:4326" ) : wmsOutputCrsList.first() };
info["crs"] = crs.toStdString();
// Typenames for WMS
const bool useIds { QgsServerProjectUtils::wmsUseLayerIds( p ) };
QStringList typenames;
const QStringList restrictedWms { QgsServerProjectUtils::wmsRestrictedLayers( p ) };
const auto constLayers { p.mapLayers().values( ) };
for ( const auto &l : constLayers )
{
if ( ! restrictedWms.contains( l->name() ) )
{
typenames.push_back( useIds ? l->id() : l->name() );
}
}
// Extent
QgsRectangle extent { QgsServerProjectUtils::wmsExtent( p ) };
QgsCoordinateReferenceSystem targetCrs;
if ( crs.split( ':' ).count() == 2 )
targetCrs = QgsCoordinateReferenceSystem::fromEpsgId( crs.split( ':' ).last().toLong() );
if ( extent.isNull() && crs.split( ':' ).count() == 2 )
{
for ( const auto &l : constLayers )
{
if ( ! restrictedWms.contains( l->name() ) )
{
QgsRectangle layerExtent { l->extent() };
if ( l->crs() != targetCrs && targetCrs.isValid() )
{
QgsCoordinateTransform ct { l->crs(), targetCrs, p.transformContext() };
layerExtent = ct.transform( layerExtent );
}
if ( extent.isNull() )
{
extent = layerExtent;
}
else
{
extent.combineExtentWith( layerExtent );
}
}
}
}
else if ( ! extent.isNull() )
{
if ( targetCrs.isValid() && targetCrs != p.crs() )
{
QgsCoordinateTransform ct { p.crs(), targetCrs, p.transformContext() };
extent = ct.transform( extent );
}
}
info["extent"] = json::array( { extent.xMinimum(), extent.yMinimum(), extent.xMaximum(), extent.yMaximum() } );
QgsRectangle geographicExtent { extent };
if ( targetCrs.authid() != 4326 )
{
QgsCoordinateTransform ct { targetCrs, QgsCoordinateReferenceSystem::fromEpsgId( 4326 ), p.transformContext() };
geographicExtent = ct.transform( geographicExtent );
}
info["geographic_extent"] = json::array( { geographicExtent.xMinimum(), geographicExtent.yMinimum(), geographicExtent.xMaximum(), geographicExtent.yMaximum() } );
// Metadata
json metadata;
const QgsProjectMetadata &md { p.metadata() };
metadata["tile"] = md.title().toStdString();
metadata["identifier"] = md.identifier().toStdString();
metadata["parentIdentifier"] = md.parentIdentifier().toStdString();
metadata["abstract"] = md.abstract().toStdString();
metadata["author"] = md.author().toStdString();
metadata["language"] = md.language().toStdString();
metadata["categories"] = jList( md.categories() );
metadata["history"] = jList( md.history() );
metadata["type"] = md.type().toStdString();
// Links
metadata["links"] = jLinksList( md.links() );
// Contacts
metadata["contacts"] = jContactList( md.contacts() );
info[ "metadata" ] = metadata;
// Capabilities
json capabilities = json::object();
capabilities["owsServiceCapabilities"] = QgsServerProjectUtils::owsServiceCapabilities( p );
capabilities["owsServiceAbstract"] = QgsServerProjectUtils::owsServiceAbstract( p ).toStdString();
capabilities["owsServiceAccessConstraints"] = QgsServerProjectUtils::owsServiceAccessConstraints( p ).toStdString();
capabilities["owsServiceContactMail"] = QgsServerProjectUtils::owsServiceContactMail( p ).toStdString();
capabilities["owsServiceContactOrganization"] = QgsServerProjectUtils::owsServiceContactOrganization( p ).toStdString();
capabilities["owsServiceContactPerson"] = QgsServerProjectUtils::owsServiceContactPerson( p ).toStdString();
capabilities["owsServiceContactPhone"] = QgsServerProjectUtils::owsServiceContactPhone( p ).toStdString();
capabilities["owsServiceContactPosition"] = QgsServerProjectUtils::owsServiceContactPosition( p ).toStdString();
capabilities["owsServiceFees"] = QgsServerProjectUtils::owsServiceFees( p ).toStdString();
capabilities["owsServiceKeywords"] = jList( QgsServerProjectUtils::owsServiceKeywords( p ) );
capabilities["owsServiceOnlineResource"] = QgsServerProjectUtils::owsServiceOnlineResource( p ).toStdString();
capabilities["owsServiceTitle"] = QgsServerProjectUtils::owsServiceTitle( p ).toStdString();
capabilities["wcsLayerIds"] = jList( QgsServerProjectUtils::wcsLayerIds( p ) );
capabilities["wcsServiceUrl"] = QgsServerProjectUtils::wcsServiceUrl( p ).toStdString();
capabilities["wfsLayerIds"] = jList( QgsServerProjectUtils::wfsLayerIds( p ) );
capabilities["wfsServiceUrl"] = QgsServerProjectUtils::wfsServiceUrl( p ).toStdString();
capabilities["wfstDeleteLayerIds"] = jList( QgsServerProjectUtils::wfstDeleteLayerIds( p ) );
capabilities["wfstInsertLayerIds"] = jList( QgsServerProjectUtils::wfstInsertLayerIds( p ) );
capabilities["wfstUpdateLayerIds"] = jList( QgsServerProjectUtils::wfstUpdateLayerIds( p ) );
capabilities["wmsDefaultMapUnitsPerMm"] = QgsServerProjectUtils::wmsDefaultMapUnitsPerMm( p );
// Skip wmsExtent because it's already in "extent"
capabilities["wmsFeatureInfoAddWktGeometry"] = QgsServerProjectUtils::wmsFeatureInfoAddWktGeometry( p );
capabilities["wmsFeatureInfoDocumentElement"] = QgsServerProjectUtils::wmsFeatureInfoDocumentElement( p ).toStdString();
capabilities["wmsFeatureInfoDocumentElementNs"] = QgsServerProjectUtils::wmsFeatureInfoDocumentElementNs( p ).toStdString();
capabilities["wmsFeatureInfoLayerAliasMap"] = jHash( QgsServerProjectUtils::wmsFeatureInfoLayerAliasMap( p ) );
capabilities["wmsFeatureInfoPrecision"] = QgsServerProjectUtils::wmsFeatureInfoPrecision( p );
capabilities["wmsFeatureInfoSchema"] = QgsServerProjectUtils::wmsFeatureInfoSchema( p ).toStdString();
capabilities["wmsFeatureInfoSegmentizeWktGeometry"] = QgsServerProjectUtils::wmsFeatureInfoSegmentizeWktGeometry( p );
capabilities["wmsImageQuality"] = QgsServerProjectUtils::wmsImageQuality( p );
capabilities["wmsInfoFormatSia2045"] = QgsServerProjectUtils::wmsInfoFormatSia2045( p );
capabilities["wmsInspireActivate"] = QgsServerProjectUtils::wmsInspireActivate( p );
capabilities["wmsInspireLanguage"] = QgsServerProjectUtils::wmsInspireLanguage( p ).toStdString();
capabilities["wmsInspireMetadataDate"] = QgsServerProjectUtils::wmsInspireMetadataDate( p ).toStdString();
capabilities["wmsInspireMetadataUrl"] = QgsServerProjectUtils::wmsInspireMetadataUrl( p ).toStdString();
capabilities["wmsInspireMetadataUrlType"] = QgsServerProjectUtils::wmsInspireMetadataUrlType( p ).toStdString();
capabilities["wmsInspireTemporalReference"] = QgsServerProjectUtils::wmsInspireTemporalReference( p ).toStdString();
capabilities["wmsMaxAtlasFeatures"] = QgsServerProjectUtils::wmsMaxAtlasFeatures( p );
capabilities["wmsMaxHeight"] = QgsServerProjectUtils::wmsMaxHeight( p );
capabilities["wmsMaxWidth"] = QgsServerProjectUtils::wmsMaxWidth( p );
capabilities["wmsOutputCrsList"] = jList( QgsServerProjectUtils::wmsOutputCrsList( p ) );
capabilities["wmsRestrictedComposers"] = jList( QgsServerProjectUtils::wmsRestrictedComposers( p ) );
capabilities["wmsRestrictedLayers"] = jList( QgsServerProjectUtils::wmsRestrictedLayers( p ) );
capabilities["wmsRootName"] = QgsServerProjectUtils::wmsRootName( p ).toStdString();
capabilities["wmsServiceUrl"] = QgsServerProjectUtils::wmsServiceUrl( p ).toStdString();
capabilities["wmsTileBuffer"] = QgsServerProjectUtils::wmsTileBuffer( p );
capabilities["wmsUseLayerIds"] = QgsServerProjectUtils::wmsUseLayerIds( p );
capabilities["wmtsServiceUrl" ] = QgsServerProjectUtils::wmtsServiceUrl( p ).toStdString();
info["capabilities"] = capabilities;
// WMS layers
info[ "wms_root_name" ] = QgsServerProjectUtils::wmsRootName( p ).toStdString();
if ( QgsServerProjectUtils::wmsRootName( p ).isEmpty() )
{
info[ "wms_root_name" ] = title.toStdString();
}
json wmsLayers;
// For convenience:
// Map layer (short) name to layer id
json wmsLayersTypenameIdMap;
// Map layer title to layer (short) name (or id if use_ids)
json wmsLayersMap;
QStringList wmsLayersSearchable;
QStringList wmsLayersQueryable;
for ( const auto &l : constLayers )
{
if ( ! restrictedWms.contains( l->name() ) )
{
json wmsLayer
{
{ "name", l->name().toStdString() },
{ "id", l->id().toStdString() },
{ "crs", l->crs().authid().toStdString() },
{ "type", l->type() == QgsMapLayerType::VectorLayer ? "vector" : "raster" },
};
if ( l->type() == QgsMapLayerType::VectorLayer )
{
const QgsVectorLayer *vl = static_cast<const QgsVectorLayer *>( l );
wmsLayer[ "pk" ] = vl->primaryKeyAttributes();
int fieldIdx { 0 };
json fieldsData;
const auto &cFields { vl->fields() };
for ( const auto &f : cFields )
{
if ( vl->excludeAttributesWfs().contains( vl->name() ) )
{
++fieldIdx;
continue;
}
const auto &constraints { f.constraints().constraints() };
const bool notNull { constraints &QgsFieldConstraints::Constraint::ConstraintNotNull &&
f.constraints().constraintStrength( QgsFieldConstraints::Constraint::ConstraintNotNull ) == QgsFieldConstraints::ConstraintStrength::ConstraintStrengthHard };
const bool unique { constraints &QgsFieldConstraints::Constraint::ConstraintUnique &&
f.constraints().constraintStrength( QgsFieldConstraints::Constraint::ConstraintUnique ) == QgsFieldConstraints::ConstraintStrength::ConstraintStrengthHard };
const bool hasExpression { constraints &QgsFieldConstraints::Constraint::ConstraintExpression &&
f.constraints().constraintStrength( QgsFieldConstraints::Constraint::ConstraintExpression ) == QgsFieldConstraints::ConstraintStrength::ConstraintStrengthHard };
const QString &defaultValue { vl->dataProvider()->defaultValueClause( fieldIdx ) };
fieldsData[ f.name().toStdString() ] =
{
{ "type", f.typeName().toStdString() },
{ "label", f.alias().isEmpty() ? f.name().toStdString() : f.alias().toStdString() },
{ "precision", f.precision() },
{ "length", f.length() },
{ "unique", unique },
{ "not_null", notNull },
{ "has_expression", hasExpression },
{ "default", defaultValue.toStdString() },
{ "expression", f.constraints().constraintExpression().toStdString() },
{ "editable", !( notNull &&unique && ! defaultValue.isEmpty() ) }
};
++fieldIdx;
}
wmsLayer[ "fields" ] = fieldsData;
}
wmsLayer[ "extent" ] = {{ l->extent().xMinimum(), l->extent().yMinimum(), l->extent().xMaximum(), l->extent().yMaximum() }};
wmsLayers[ l->id().toStdString() ] = wmsLayer;
// Fill maps
const QString name { l->title().isEmpty() ? l->name() : l->title() };
const QString shortName { l->shortName().isEmpty() ? l->shortName() : l->name() };
wmsLayersTypenameIdMap[ shortName.toStdString()] = l->id().toStdString();
wmsLayersMap[ name.toStdString() ] = useIds ? l->id().toStdString() : shortName.toStdString();
if ( l->flags() & QgsMapLayer::Searchable )
{
wmsLayersSearchable.push_back( l->id() );
}
if ( l->flags() & QgsMapLayer::Identifiable )
{
wmsLayersQueryable.push_back( l->id() );
}
// Layer metadata
json layerMetadata;
const auto &md { l->metadata() };
layerMetadata[ "abstract" ] = md.abstract( ).toStdString();
layerMetadata[ "categories" ] = jList( md.categories( ) );
layerMetadata[ "contacts" ] = jContactList( md.contacts() );
layerMetadata[ "encoding" ] = md.encoding( ).toStdString();
layerMetadata[ "fees" ] = md.fees( ).toStdString();
layerMetadata[ "history" ] = jList( md.history( ) );
layerMetadata[ "identifier" ] = md.identifier( ).toStdString();
layerMetadata[ "keywordVocabularies" ] = jList( md.keywordVocabularies( ) );
// Keywords
const auto &cKw { md.keywords().keys() };
json jKeywords;
for ( const auto &k : cKw )
{
jKeywords[ k.toStdString() ] = jList( md.keywords()[ k ] );
}
layerMetadata[ "keywords" ] = jKeywords;
layerMetadata[ "language" ] = md.language( ).toStdString();
layerMetadata[ "licenses" ] = jList( md.licenses( ) );
layerMetadata[ "links" ] = jLinksList( md.links( ) );
layerMetadata[ "parentIdentifier" ] = md.parentIdentifier( ).toStdString();
layerMetadata[ "rights" ] = jList( md.rights( ) );
layerMetadata[ "title" ] = md.title( ).toStdString();
layerMetadata[ "type" ] = md.type( ).toStdString();
}
}
info[ "wms_layers" ] = wmsLayers;
info[ "wms_layers_map" ] = wmsLayersMap;
info[ "wms_layers_queryable" ] = jList( wmsLayersQueryable );
info[ "wms_layers_searchable" ] = jList( wmsLayersSearchable );
info[ "wms_layers_typename_id_map" ] = wmsLayersTypenameIdMap;
info[ "toc " ] = layerTree( p, wmsLayersQueryable, wmsLayersSearchable );
}
else
{
QgsMessageLog::logMessage( QStringLiteral( "Could not read project '%1': skipping." ).arg( projectPath ), QStringLiteral( "Server" ), Qgis::MessageLevel::Warning );
}
return info;
}
json QgsLandingPageUtils::layerTree( const QgsProject &project, const QStringList &wmsLayersQueryable, const QStringList &wmsLayersSearchable )
{
const bool useIds { QgsServerProjectUtils::wmsUseLayerIds( project ) };
const QStringList wmsRestrictedLayers { QgsServerProjectUtils::wmsRestrictedLayers( project ) };
const QStringList wfsLayerIds { QgsServerProjectUtils::wfsLayerIds( project ) };
std::function<json( const QgsLayerTreeNode *, const QString & )> harvest = [ & ]( const QgsLayerTreeNode * node, const QString & parentId ) -> json
{
const std::string nodeName { parentId.isEmpty() ? "root" : node->name().toStdString() };
QString title { QString::fromStdString( nodeName ) };
json rec {
{ "title", nodeName },
{ "name", nodeName },
{ "expanded", node->isExpanded() },
{ "visible", node->isVisible() },
};
if ( QgsLayerTree::isLayer( node ) )
{
const QgsLayerTreeLayer *l { static_cast<const QgsLayerTreeLayer *>( node ) };
if ( l->layer()->type() == QgsMapLayerType::VectorLayer || l->layer()->type() == QgsMapLayerType::RasterLayer )
{
rec[ "id" ] = l->layerId().toStdString();
rec[ "queryable" ] = wmsLayersQueryable.contains( l->layerId() );
rec[ "searchable" ] = wmsLayersSearchable.contains( l->layerId() );
rec[ "wfs_enabled" ] = wfsLayerIds.contains( l->layerId() );
const QString layerName { l->layer()->shortName().isEmpty() ? l->layer()->name() : l->layer()->shortName()};
rec[ "typename" ] = useIds ? l->layer()->id().toStdString() : layerName.toStdString();
// Override title
if ( ! l->layer()->title().isEmpty() )
{
title = l->layer()->title();
}
}
else
{
// Unsupported layer
return nullptr;
}
rec[ "is_layer" ] = true;
}
else
{
rec[ "is_layer" ] = false;
}
rec[ "title"] = title.toStdString();
const QString treeId = parentId == QStringLiteral( "root" ) ? QStringLiteral( "root" ) : parentId + "." + title;
rec[ "tree_id" ] = treeId.toStdString();
rec[ "tree_id_hash" ] = QCryptographicHash::hash( treeId.toUtf8(), QCryptographicHash::Md5 ).toHex().toStdString();
// Collect children
json children = json::array();
const auto cChildren { node->children() };
for ( const auto &c : cChildren )
{
const json harvested { harvest( c, treeId ) };
if ( ! harvested.is_null() )
{
children.push_back( harvested );
}
}
rec [ "children" ] = children;
return rec;
};
return harvest( project.layerTreeRoot(), QString() );
}
QString QgsLandingPageUtils::projectPathFromUrl( const QString &url )
{
const auto match { QgsLandingPageUtils::PROJECT_HASH_RE.match( url ) };
if ( match.hasMatch() )
{
const auto availableProjects { QgsLandingPageUtils::projects() };
return availableProjects.value( match.captured( QStringLiteral( "projectHash" ) ), QString() );
}
return QString();
};

View File

@ -0,0 +1,72 @@
/***************************************************************************
qgslandingpageutils.h - QgsLandingPageUtils
---------------------
begin : 3.8.2020
copyright : (C) 2020 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. *
* *
***************************************************************************/
#ifndef QGSLANDINGPAGEUTILS_H
#define QGSLANDINGPAGEUTILS_H
#include <QMap>
#include <QStringList>
#include <QRegularExpression>
#include "nlohmann/json_fwd.hpp"
#ifndef SIP_RUN
using namespace nlohmann;
#endif
class QgsProject;
/**
* The QgsLandingPageUtils struct contains static utilities for the
* landing page plugin
*/
struct QgsLandingPageUtils
{
/**
* Returns a list of available projects from various sources:
*
* - QGIS_SERVER_PROJECTS_DIRECTORIES directories
* - QGIS_SERVER_PROJECTS_PG_CONNECTIONS postgres connections
*
* Multiple paths and connections may be separated by two pipe chars: '||'
*
* \returns hash of project paths (or other storage identifiers) with a digest key
*/
static QMap<QString, QString> projects();
/**
* Returns project information for a given \a projectPath
*/
static json projectInfo( const QString &projectPath );
/**
* Returns the layer tree information for the given \a project
*/
static json layerTree( const QgsProject &project, const QStringList &wmsLayersQueryable, const QStringList &wmsLayersSearchable );
/**
* Extracts the project hash from the URL and returns the (possibly empty) project path.
*/
static QString projectPathFromUrl( const QString &url );
/**
* PROJECTS_RE regex to extract project hash from URL
*/
static const QRegularExpression PROJECT_HASH_RE;
};
#endif // QGSLANDINGPAGEUTILS_H

View File

@ -137,6 +137,15 @@ class QgsServerTestBase(unittest.TestCase):
pass
self.server = QgsServer()
# Disable landing page API to test standard legacy XML responses in case of errors
os.environ["QGIS_SERVER_DISABLED_APIS"] = "Landing Page"
def tearDown(self):
""""Cleanup env"""
super().tearDown()
del os.environ["QGIS_SERVER_DISABLED_APIS"]
def strip_version_xmlns(self, text):
"""Order of attributes is random, strip version and xmlns"""
return text.replace(b'version="1.3.0"', b'').replace(b'xmlns="http://www.opengis.net/ogc"', b'')

View File

@ -182,14 +182,14 @@ class QgsServerAPITestBase(QgsServerTestBase):
result.append(bytes(response.body()).decode('utf8'))
return '\n'.join(result)
def compareApi(self, request, project, reference_file):
def compareApi(self, request, project, reference_file, subdir='api'):
response = QgsBufferServerResponse()
# Add json to accept it reference_file is JSON
if reference_file.endswith('.json'):
request.setHeader('Accept', 'application/json')
self.server.handleRequest(request, response, project)
result = bytes(response.body()).decode('utf8') if reference_file.endswith('html') else self.dump(response)
path = unitTestDataPath('qgis_server') + '/api/' + reference_file
path = os.path.join(unitTestDataPath('qgis_server'), subdir, reference_file)
if self.regeregenerate_api_reference:
# Try to change timestamp
try: