Support for custom URL prefix for landing page

Add a QGIS_SERVER_LANDING_PAGE_PREFIX server
setting:

Prefix of the path component of the landing page base URL, default is empty (since QGIS 3.20).
This commit is contained in:
Alessandro Pasotti 2021-05-06 18:15:04 +02:00
parent 497197e316
commit 27bfb09fb8
19 changed files with 108 additions and 42 deletions

View File

@ -1 +1 @@
<!DOCTYPE html><html lang=it><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-06c8fa3c.09a55b1d.css rel=prefetch><link href=css/chunk-123bc409.8679d8ba.css rel=prefetch><link href=css/chunk-744799cf.0a230f6c.css rel=prefetch><link href=js/chunk-06c8fa3c.010c72ad.js rel=prefetch><link href=js/chunk-123bc409.ec41f71a.js rel=prefetch><link href=js/chunk-744799cf.70663d3c.js rel=prefetch><link href=css/app.ca3f5643.css rel=preload as=style><link href=css/chunk-vendors.a728f495.css rel=preload as=style><link href=js/app.333c53e0.js rel=preload as=script><link href=js/chunk-vendors.573fc8d0.js rel=preload as=script><link href=css/chunk-vendors.a728f495.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.573fc8d0.js></script><script src=js/app.333c53e0.js></script></body></html>
<!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-06c8fa3c.09a55b1d.css rel=prefetch><link href=css/chunk-123bc409.8679d8ba.css rel=prefetch><link href=css/chunk-a28d6c70.162a27d1.css rel=prefetch><link href=js/chunk-06c8fa3c.010c72ad.js rel=prefetch><link href=js/chunk-123bc409.ec41f71a.js rel=prefetch><link href=js/chunk-a28d6c70.7ed0c6db.js rel=prefetch><link href=css/app.ca3f5643.css rel=preload as=style><link href=css/chunk-vendors.a728f495.css rel=preload as=style><link href=js/app.3a5ac8de.js rel=preload as=script><link href=js/chunk-vendors.573fc8d0.js rel=preload as=script><link href=css/chunk-vendors.a728f495.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.573fc8d0.js></script><script src=js/app.3a5ac8de.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

View File

@ -7,7 +7,7 @@
name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no"
/>
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<link rel="icon" href="./favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
<link
rel="stylesheet"

View File

@ -181,7 +181,7 @@ export default {
filter = `&${this.filterField.value}=${this.filterText}`;
}
fetch(
`/project/${this.project.id}/wfs3/collections/${this.typename}/items.json?limit=5&offset=${offset}${sorting}${filter}`
`./project/${this.project.id}/wfs3/collections/${this.typename}/items.json?limit=5&offset=${offset}${sorting}${filter}`
)
.then(response => {
if (!response) {
@ -241,4 +241,4 @@ export default {
.btn-close {
float: right;
}
</style>
</style>

View File

@ -73,7 +73,7 @@ export default new Vuex.Store({
actions: {
async getCatalog({ commit }) {
try {
fetch(`/index.json`)
fetch(`./index.json`)
.then((response) => {
if (!response) {
throw Error(`Error fetching data from QGIS Server`)
@ -100,8 +100,7 @@ export default new Vuex.Store({
},
async getProject({ commit }, projectId) {
try {
//console.log(`Inside getProject ${projectId}`)
fetch(`/map/${projectId}.json`)
fetch(`./map/${projectId}.json`)
.then((response) => {
if (!response) {
throw Error(`Error fetching data from QGIS Server`)
@ -130,7 +129,7 @@ export default new Vuex.Store({
* Fetches the TOC style icons from GetLegendGraphics
*/
async getToc({ commit }, payload) {
let toc_url = `/project/${payload.projectId}/?SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=${payload.layers}&FORMAT=application/json`
let toc_url = `./project/${payload.projectId}/?SERVICE=WMS&REQUEST=GetLegendGraphics&LAYERS=${payload.layers}&FORMAT=application/json`
fetch(toc_url)
.then(this.handleErrors)
.then((response) => response.json())

View File

@ -313,7 +313,7 @@ export default {
this.map.fitBounds(jl.getBounds());
}
let that = this;
this.wms_source = WmsSource.source(`/project/` + project.id + `/?`, {
this.wms_source = WmsSource.source(`./project/` + project.id + `/?`, {
tileSize: 512,
transparent: true,
format: "image/png",
@ -499,4 +499,4 @@ export default {
.expanded-sidebar .leaflet-left {
left: 300px !important;
}
</style>
</style>

View File

@ -1,7 +1,5 @@
module.exports = {
publicPath: process.env.NODE_ENV === 'production'
? './'
: '/',
publicPath: './',
assetsDir: './',
configureWebpack: {
devtool: "source-map",

View File

@ -84,18 +84,18 @@ void QgsServerOgcApi::executeRequest( const QgsServerApiContext &context ) const
auto path { sanitizeUrl( context.request()->url() ).path() };
// Find matching handler
auto hasMatch { false };
for ( const auto &h : mHandlers )
for ( const auto &handler : mHandlers )
{
QgsMessageLog::logMessage( QStringLiteral( "Checking API path %1 for %2 " ).arg( path, h->path().pattern() ), QStringLiteral( "Server" ), Qgis::Info );
if ( h->path().match( path ).hasMatch() )
QgsMessageLog::logMessage( QStringLiteral( "Checking API path %1 for %2 " ).arg( path, handler->path().pattern() ), QStringLiteral( "Server" ), Qgis::Info );
if ( handler->path().match( path ).hasMatch() )
{
hasMatch = true;
// Execute handler
QgsMessageLog::logMessage( QStringLiteral( "API %1: found handler %2" ).arg( name(), QString::fromStdString( h->operationId() ) ), QStringLiteral( "Server" ), Qgis::Info );
QgsMessageLog::logMessage( QStringLiteral( "API %1: found handler %2" ).arg( name(), QString::fromStdString( handler->operationId() ) ), QStringLiteral( "Server" ), Qgis::Info );
// May throw QgsServerApiBadRequestException or JSON exceptions on serializing
try
{
h->handleRequest( context );
handler->handleRequest( context );
}
catch ( json::exception &ex )
{

View File

@ -46,8 +46,8 @@ class QgsLandingPageApi: public QgsServerOgcApi
{
QString baseUrlPrefix{ serverIface()->serverSettings()->landingPageBaseUrlPrefix() };
// Make sure prefix always starts with /
if ( ! baseUrlPrefix.startsWith( '/' ) )
// Make sure non empty prefix always starts with /
if ( ! baseUrlPrefix.isEmpty() && ! baseUrlPrefix.startsWith( '/' ) )
{
baseUrlPrefix.prepend( '/' );
}
@ -69,11 +69,11 @@ class QgsLandingPageApi: public QgsServerOgcApi
return path.isEmpty()
|| path == '/'
|| path.startsWith( QLatin1String( "/map/" ) )
|| path.startsWith( QLatin1String( "/index" ) )
|| path.startsWith( QLatin1String( "/index." ) )
// Static
|| path.startsWith( QLatin1String( "/css/" ) )
|| path.startsWith( QLatin1String( "/js/" ) )
|| path == QLatin1String( "/favicon.ico" );
|| path == QLatin1String( "favicon.ico" );
}
};
@ -100,7 +100,7 @@ class QgsProjectLoaderFilter: public QgsServerFilter
{
mEnvWasChanged = false;
const auto handler { serverInterface()->requestHandler() };
if ( handler->path().startsWith( QStringLiteral( "%1project/" ).arg( QgsLandingPageHandler::prefix( serverInterface()->serverSettings() ) ) ) )
if ( handler->path().startsWith( QStringLiteral( "%1/project/" ).arg( QgsLandingPageHandler::prefix( serverInterface()->serverSettings() ) ) ) )
{
const QString projectPath { QgsLandingPageUtils::projectUriFromUrl( handler->url(), *serverInterface()->serverSettings() ) };
if ( ! projectPath.isEmpty() )
@ -153,7 +153,7 @@ class QgsLandingPageModule: public QgsServiceModule
};
// Register handlers
landingPageApi->registerHandler<QgsServerStaticHandler>(
QStringLiteral( "%1(?<staticFilePath>((css|js)/.*)|favicon.ico)$" )
QStringLiteral( "%1/(?<staticFilePath>((css|js)/.*)|favicon.ico)$" )
.arg( QgsLandingPageHandler::prefix( serverIface->serverSettings() ) ), QStringLiteral( "landingpage" ) );
landingPageApi->registerHandler<QgsLandingPageHandler>( serverIface->serverSettings() );
landingPageApi->registerHandler<QgsLandingPageMapHandler>( serverIface->serverSettings() );

View File

@ -35,12 +35,20 @@ QgsLandingPageHandler::QgsLandingPageHandler( const QgsServerSettings *settings
void QgsLandingPageHandler::handleRequest( const QgsServerApiContext &context ) const
{
if ( context.request()->url().path( ) == prefix( context.serverInterface()->serverSettings() ) )
const QString requestPrefix { prefix( context.serverInterface()->serverSettings() ) };
auto urlPath { context.request()->url().path( ) };
while ( urlPath.endsWith( '/' ) )
{
urlPath.chop( 1 );
}
if ( urlPath == requestPrefix )
{
QUrl url { context.request()->url() };
url.setPath( QStringLiteral( "%1index.%2" )
.arg( prefix( context.serverInterface()->serverSettings() ) )
.arg( QgsServerOgcApi::contentTypeToExtension( contentTypeFromRequest( context.request() ) ) ) );
url.setPath( QStringLiteral( "%1/index.%2" )
.arg( requestPrefix,
QgsServerOgcApi::contentTypeToExtension( contentTypeFromRequest( context.request() ) ) ) );
context.response()->setStatusCode( 302 );
context.response()->setHeader( QStringLiteral( "Location" ), url.toString() );
}
@ -67,11 +75,13 @@ const QString QgsLandingPageHandler::templatePath( const QgsServerApiContext &co
QString QgsLandingPageHandler::prefix( const QgsServerSettings *settings )
{
QString prefix { settings->landingPageBaseUrlPrefix() };
if ( prefix.endsWith( '/' ) )
while ( prefix.endsWith( '/' ) )
{
prefix.remove( prefix.length() - 1 );
prefix.chop( 1 );
}
if ( ! prefix.startsWith( '/' ) )
if ( ! prefix.isEmpty() && ! prefix.startsWith( '/' ) )
{
prefix.prepend( '/' );
}
@ -116,8 +126,13 @@ void QgsLandingPageMapHandler::handleRequest( const QgsServerApiContext &context
write( data, context, {{ "pageTitle", linkTitle() }, { "navigation", json::array() }} );
}
QRegularExpression QgsLandingPageMapHandler::path() const
{
return QRegularExpression( QStringLiteral( R"re(^%1/map/([a-f0-9]{32}).*$)re" ).arg( QgsLandingPageHandler::prefix( mSettings ) ) );
}
QRegularExpression QgsLandingPageHandler::path() const
{
return QRegularExpression( QStringLiteral( R"re(^%1(/index.html|/index.json)?$)re" ).arg( prefix( mSettings ) ) );
return QRegularExpression( QStringLiteral( R"re(^%1(/index.html|/index.json|/)?$)re" ).arg( prefix( mSettings ) ) );
}

View File

@ -54,8 +54,13 @@ class QgsLandingPageHandler: public QgsServerOgcApiHandler
QgsServerOgcApi::Rel linkType() const override { return QgsServerOgcApi::Rel::self; }
const QString templatePath( const QgsServerApiContext &context ) const override;
/**
* Returns the path prefix, default is empty. Also makes sure that not-empty
* prefix starts with "/" (ex: "/mylandingprefix"
*/
static QString prefix( const QgsServerSettings *settings );
private:
@ -77,7 +82,7 @@ class QgsLandingPageMapHandler: public QgsServerOgcApiHandler
void handleRequest( const QgsServerApiContext &context ) const override;
// QgsServerOgcApiHandler interface
QRegularExpression path() const override { return QRegularExpression( R"re(^/map/([a-f0-9]{32}).*$)re" ); }
QRegularExpression path() const override;
std::string operationId() const override { return "getMap"; }
QStringList tags() const override { return { QStringLiteral( "Catalog" ), QStringLiteral( "Map Viewer" ) }; }
std::string summary() const override

View File

@ -67,13 +67,22 @@ class QgsServerLandingPageTest(QgsServerAPITestBase):
"""Setup env"""
super().setUp()
os.environ["QGIS_SERVER_DISABLED_APIS"] = ''
try:
del (os.environ["QGIS_SERVER_DISABLED_APIS"])
except:
pass
try:
del (os.environ['QGIS_SERVER_LANDING_PAGE_PREFIX'])
except:
pass
os.environ['QGIS_SERVER_LANDING_PAGE_PROJECTS_DIRECTORIES'] = '||'.join(self.directories)
if not os.environ.get('TRAVIS', False):
os.environ['QGIS_SERVER_LANDING_PAGE_PROJECTS_PG_CONNECTIONS'] = "postgresql://localhost:5432?sslmode=disable&dbname=landing_page_test&schema=public"
def test_landing_page_redirects(self):
def ___test_landing_page_redirects(self):
"""Test landing page redirects"""
request = QgsBufferServerRequest('http://server.qgis.org/')
@ -82,6 +91,14 @@ class QgsServerLandingPageTest(QgsServerAPITestBase):
self.server.handleRequest(request, response)
self.assertEqual(response.headers()[
'Location'], 'http://server.qgis.org/index.json')
request = QgsBufferServerRequest('http://server.qgis.org')
request.setHeader('Accept', 'application/json')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response)
self.assertEqual(response.headers()[
'Location'], 'http://server.qgis.org/index.json')
response = QgsBufferServerResponse()
request.setHeader('Accept', 'text/html')
self.server.handleRequest(request, response)
@ -165,6 +182,38 @@ class QgsServerLandingPageTest(QgsServerAPITestBase):
self.compareApi(
request, None, 'test_landing_page_empty_index.json', subdir='landingpage')
def test_landing_page_prefix(self):
os.environ['QGIS_SERVER_LANDING_PAGE_PREFIX'] = '/mylanding'
def _test_error(uri):
request = QgsBufferServerRequest(uri)
request.setHeader('Accept', 'application/json')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response)
self.assertEqual(bytes(response.body()), b'<ServerException>Project file error. For OWS services: please provide a SERVICE and a MAP parameter pointing to a valid QGIS project file</ServerException>\n')
_test_error('http://server.qgis.org/index.json')
_test_error('http://server.qgis.org/index.html')
_test_error('http://server.qgis.org/')
_test_error('http://server.qgis.org')
def _test_valid(uri):
request = QgsBufferServerRequest(uri)
request.setHeader('Accept', 'application/json')
response = QgsBufferServerResponse()
self.server.handleRequest(request, response)
if 'index' not in uri:
self.assertEqual(response.headers()[
'Location'], 'http://server.qgis.org/mylanding/index.json')
else:
# Just check that it's valid json
j = json.loads(bytes(response.body()))
_test_valid('http://server.qgis.org/mylanding')
_test_valid('http://server.qgis.org/mylanding/')
_test_valid('http://server.qgis.org/mylanding/index.json')
if __name__ == '__main__':
unittest.main()