Skip to content

Commit

Permalink
Merge pull request #38039 from qgis-bot/backport-38034-to-release-3_14
Browse files Browse the repository at this point in the history
[Backport release-3_14] PG style storage: replace forbidden XML unicode chars
  • Loading branch information
elpaso committed Jul 30, 2020
2 parents 1763d33 + cdb9dd2 commit 46ebfaf
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 2 deletions.
2 changes: 1 addition & 1 deletion src/core/qgsdatasourceuri.cpp
Expand Up @@ -159,7 +159,7 @@ QgsDataSourceUri::QgsDataSourceUri( const QString &u )
}
else if ( pname == QLatin1String( "connect_timeout" ) )
{
QgsDebugMsg( QStringLiteral( "connection timeout ignored" ) );
QgsDebugMsgLevel( QStringLiteral( "connection timeout ignored" ), 3 );
}
else if ( pname == QLatin1String( "dbname" ) )
{
Expand Down
40 changes: 39 additions & 1 deletion src/providers/postgres/qgspostgresprovider.cpp
Expand Up @@ -707,6 +707,34 @@ QString QgsPostgresUtils::andWhereClauses( const QString &c1, const QString &c2
return QStringLiteral( "(%1) AND (%2)" ).arg( c1, c2 );
}

void QgsPostgresUtils::replaceInvalidXmlChars( QString &xml )
{
static const QRegularExpression replaceRe { QStringLiteral( "([\x00-\x08\x0B-\x1F\x7F])" ) };
QRegularExpressionMatchIterator it {replaceRe.globalMatch( xml ) };
while ( it.hasNext() )
{
const QRegularExpressionMatch match { it.next() };
const QChar c { match.captured( 1 ).at( 0 ) };
xml.replace( c, QStringLiteral( "UTF-8[%1]" ).arg( c.unicode() ) );
}
}

void QgsPostgresUtils::restoreInvalidXmlChars( QString &xml )
{
static const QRegularExpression replaceRe { QStringLiteral( R"raw(UTF-8\[(\d+)\])raw" ) };
QRegularExpressionMatchIterator it {replaceRe.globalMatch( xml ) };
while ( it.hasNext() )
{
const QRegularExpressionMatch match { it.next() };
bool ok;
const ushort code { match.captured( 1 ).toUShort( &ok ) };
if ( ok )
{
xml.replace( QStringLiteral( "UTF-8[%1]" ).arg( code ), QChar( code ) );
}
}
}

QString QgsPostgresProvider::filterWhereClause() const
{
QString where;
Expand Down Expand Up @@ -4985,12 +5013,18 @@ QgsVectorLayerExporter::ExportError QgsPostgresProviderMetadata::createEmptyLaye
);
}

bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString &qmlStyle, const QString &sldStyle,
bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString &qmlStyleIn, const QString &sldStyleIn,
const QString &styleName, const QString &styleDescription,
const QString &uiFileContent, bool useAsDefault, QString &errCause )
{
QgsDataSourceUri dsUri( uri );

// Replace invalid XML characters
QString qmlStyle { qmlStyleIn };
QgsPostgresUtils::replaceInvalidXmlChars( qmlStyle );
QString sldStyle { sldStyleIn };
QgsPostgresUtils::replaceInvalidXmlChars( sldStyle );

QgsPostgresConn *conn = QgsPostgresConn::connectDb( dsUri.connectionInfo( false ), false );
if ( !conn )
{
Expand Down Expand Up @@ -5236,6 +5270,8 @@ QString QgsPostgresProviderMetadata::loadStyle( const QString &uri, QString &err
QString style = result.PQntuples() == 1 ? result.PQgetvalue( 0, 0 ) : QString();
conn->unref();

QgsPostgresUtils::restoreInvalidXmlChars( style );

return style;
}

Expand Down Expand Up @@ -5383,6 +5419,8 @@ QString QgsPostgresProviderMetadata::getStyleById( const QString &uri, QString s

conn->unref();

QgsPostgresUtils::restoreInvalidXmlChars( style );

return style;
}

Expand Down
6 changes: 6 additions & 0 deletions src/providers/postgres/qgspostgresprovider.h
Expand Up @@ -522,6 +522,12 @@ class QgsPostgresUtils
{
return x <= ( ( INT32PK_OFFSET ) / 2.0 ) ? x : -( INT32PK_OFFSET - x );
}

//! Replaces invalid XML chars with UTF-8[<char_code>]
static void replaceInvalidXmlChars( QString &xml );

//! Replaces UTF-8[<char_code>] with the actual unicode char
static void restoreInvalidXmlChars( QString &xml );
};

/**
Expand Down
29 changes: 29 additions & 0 deletions tests/src/python/test_provider_postgres.py
Expand Up @@ -1719,6 +1719,35 @@ def testStyleWithGeometryType(self):
ids = styles[1]
self.assertEqual(len(ids), 0)

def testSaveStyleInvalidXML(self):

self.execSQLCommand('DROP TABLE IF EXISTS layer_styles CASCADE')

vl = self.getEditableLayer()
self.assertTrue(vl.isValid())
self.assertTrue(
vl.dataProvider().isSaveAndLoadStyleToDatabaseSupported())
self.assertTrue(vl.dataProvider().isDeleteStyleFromDatabaseSupported())

mFilePath = QDir.toNativeSeparators(
'%s/symbol_layer/%s.qml' % (unitTestDataPath(), "fontSymbol"))
status = vl.loadNamedStyle(mFilePath)
self.assertTrue(status)

errorMsg = vl.saveStyleToDatabase(
"fontSymbol", "font with invalid utf8 char", False, "")
self.assertEqual(errorMsg, "")

qml, errmsg = vl.getStyleFromDatabase("1")
self.assertTrue('v="\u001E"' in qml)
self.assertEqual(errmsg, "")

# Test loadStyle from metadata
md = QgsProviderRegistry.instance().providerMetadata('postgres')
qml = md.loadStyle(self.dbconn + " type=POINT table=\"qgis_test\".\"editData\" (geom)", 'fontSymbol')
self.assertTrue(qml.startswith('<!DOCTYPE qgi'), qml)
self.assertTrue('v="\u001E"' in qml)

def testHasMetadata(self):
# views don't have metadata
vl = QgsVectorLayer('{} table="qgis_test"."{}" key="pk" sql='.format(self.dbconn, 'bikes_view'), "bikes_view",
Expand Down
162 changes: 162 additions & 0 deletions tests/testdata/symbol_layer/fontSymbol.qml
@@ -0,0 +1,162 @@
<!DOCTYPE qgis PUBLIC 'http://mrcc.com/qgis.dtd' 'SYSTEM'>
<qgis version="3.15.0-Master" minScale="100000000" simplifyDrawingHints="0" labelsEnabled="0" hasScaleBasedVisibilityFlag="0" readOnly="0" simplifyMaxScale="1" simplifyDrawingTol="1.2" maxScale="0" styleCategories="AllStyleCategories" simplifyAlgorithm="0" simplifyLocal="1">
<flags>
<Identifiable>1</Identifiable>
<Removable>1</Removable>
<Searchable>1</Searchable>
</flags>
<temporal endField="" startExpression="" durationField="" fixedDuration="0" enabled="0" accumulate="0" durationUnit="min" endExpression="" startField="" mode="0">
<fixedRange>
<start></start>
<end></end>
</fixedRange>
</temporal>
<renderer-v2 symbollevels="0" forceraster="0" type="singleSymbol" enableorderby="0">
<symbols>
<symbol name="0" alpha="1" force_rhr="0" type="marker" clip_to_extent="1">
<layer enabled="1" class="FontMarker" pass="0" locked="0">
<prop k="angle" v="0"/>
<prop k="chr" v=""/>
<prop k="color" v="145,82,45,255"/>
<prop k="font" v="Dingbats"/>
<prop k="font_style" v=""/>
<prop k="horizontal_anchor_point" v="1"/>
<prop k="joinstyle" v="bevel"/>
<prop k="offset" v="0,0"/>
<prop k="offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="offset_unit" v="MM"/>
<prop k="outline_color" v="35,35,35,255"/>
<prop k="outline_width" v="0"/>
<prop k="outline_width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="outline_width_unit" v="MM"/>
<prop k="size" v="2"/>
<prop k="size_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="size_unit" v="MM"/>
<prop k="vertical_anchor_point" v="1"/>
<data_defined_properties>
<Option type="Map">
<Option value="" name="name" type="QString"/>
<Option name="properties"/>
<Option value="collection" name="type" type="QString"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</symbols>
<rotation/>
<sizescale/>
</renderer-v2>
<customproperties>
<property value="0" key="embeddedWidgets/count"/>
<property key="variableNames"/>
<property key="variableValues"/>
</customproperties>
<blendMode>0</blendMode>
<featureBlendMode>0</featureBlendMode>
<layerOpacity>1</layerOpacity>
<SingleCategoryDiagramRenderer attributeLegend="1" diagramType="Histogram">
<DiagramCategory maxScaleDenominator="1e+08" backgroundColor="#ffffff" penAlpha="255" penColor="#000000" scaleBasedVisibility="0" rotationOffset="270" width="15" lineSizeType="MM" labelPlacementMethod="XHeight" sizeScale="3x:0,0,0,0,0,0" spacing="5" height="15" backgroundAlpha="255" lineSizeScale="3x:0,0,0,0,0,0" diagramOrientation="Up" spacingUnitScale="3x:0,0,0,0,0,0" minScaleDenominator="0" minimumSize="0" sizeType="MM" opacity="1" spacingUnit="MM" direction="0" enabled="0" barWidth="5" showAxis="1" scaleDependency="Area" penWidth="0">
<fontProperties description="Noto Sans,10,-1,5,50,0,0,0,0,0,Regular" style="Regular"/>
<axisSymbol>
<symbol name="" alpha="1" force_rhr="0" type="line" clip_to_extent="1">
<layer enabled="1" class="SimpleLine" pass="0" locked="0">
<prop k="align_dash_pattern" v="0"/>
<prop k="capstyle" v="square"/>
<prop k="customdash" v="5;2"/>
<prop k="customdash_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="customdash_unit" v="MM"/>
<prop k="dash_pattern_offset" v="0"/>
<prop k="dash_pattern_offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="dash_pattern_offset_unit" v="MM"/>
<prop k="draw_inside_polygon" v="0"/>
<prop k="joinstyle" v="bevel"/>
<prop k="line_color" v="35,35,35,255"/>
<prop k="line_style" v="solid"/>
<prop k="line_width" v="0.26"/>
<prop k="line_width_unit" v="MM"/>
<prop k="offset" v="0"/>
<prop k="offset_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<prop k="offset_unit" v="MM"/>
<prop k="ring_filter" v="0"/>
<prop k="tweak_dash_pattern_on_corners" v="0"/>
<prop k="use_custom_dash" v="0"/>
<prop k="width_map_unit_scale" v="3x:0,0,0,0,0,0"/>
<data_defined_properties>
<Option type="Map">
<Option value="" name="name" type="QString"/>
<Option name="properties"/>
<Option value="collection" name="type" type="QString"/>
</Option>
</data_defined_properties>
</layer>
</symbol>
</axisSymbol>
</DiagramCategory>
</SingleCategoryDiagramRenderer>
<DiagramLayerSettings placement="0" priority="0" dist="0" zIndex="0" linePlacementFlags="18" showAll="1" obstacle="0">
<properties>
<Option type="Map">
<Option value="" name="name" type="QString"/>
<Option name="properties"/>
<Option value="collection" name="type" type="QString"/>
</Option>
</properties>
</DiagramLayerSettings>
<geometryOptions removeDuplicateNodes="0" geometryPrecision="0">
<activeChecks/>
<checkConfiguration/>
</geometryOptions>
<referencedLayers/>
<referencingLayers/>
<fieldConfiguration/>
<aliases/>
<excludeAttributesWMS/>
<excludeAttributesWFS/>
<defaults/>
<constraints/>
<constraintExpressions/>
<expressionfields/>
<attributeactions>
<defaultAction value="{00000000-0000-0000-0000-000000000000}" key="Canvas"/>
</attributeactions>
<attributetableconfig actionWidgetStyle="dropDown" sortOrder="0" sortExpression="">
<columns>
<column width="-1" type="actions" hidden="1"/>
</columns>
</attributetableconfig>
<conditionalstyles>
<rowstyles/>
<fieldstyles/>
</conditionalstyles>
<storedexpressions/>
<editform tolerant="1"></editform>
<editforminit/>
<editforminitcodesource>0</editforminitcodesource>
<editforminitfilepath></editforminitfilepath>
<editforminitcode><![CDATA[# -*- coding: utf-8 -*-
"""
QGIS forms can have a Python function that is called when the form is
opened.
Use this function to add extra logic to your forms.
Enter the name of the function in the "Python Init function"
field.
An example follows:
"""
from qgis.PyQt.QtWidgets import QWidget

def my_form_open(dialog, layer, feature):
geom = feature.geometry()
control = dialog.findChild(QWidget, "MyLineEdit")
]]></editforminitcode>
<featformsuppress>0</featformsuppress>
<editorlayout>generatedlayout</editorlayout>
<editable/>
<labelOnTop/>
<dataDefinedFieldProperties/>
<widgets/>
<previewExpression></previewExpression>
<mapTip></mapTip>
<layerGeometryType>0</layerGeometryType>
</qgis>

0 comments on commit 46ebfaf

Please sign in to comment.