Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions django/contrib/admin/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

from django.contrib.auth import get_user_model
from django.contrib.auth.templatetags.auth import render_password_as_hash
from django.core.exceptions import FieldDoesNotExist
from django.core.validators import EMPTY_VALUES
from django.core.exceptions import FieldDoesNotExist, ValidationError
from django.core.validators import EMPTY_VALUES, URLValidator
from django.db import models, router
from django.db.models.constants import LOOKUP_SEP
from django.db.models.deletion import Collector
Expand Down Expand Up @@ -464,6 +464,14 @@ def display_for_field(value, field, empty_value_display, avoid_link=False):
elif isinstance(field, models.FileField) and value and not avoid_link:
return format_html('<a href="{}">{}</a>', value.url, value)
elif isinstance(field, models.URLField) and value and not avoid_link:
# Only render a clickable link for URLs with a safe scheme, so that a
# potentially dangerous stored value is shown as plain text rather than
# an executable link. The check is deliberately independent of the
# field's own validators, which may permit such schemes.
try:
URLValidator()(value)
except ValidationError:
return display_for_value(value, empty_value_display)
return format_html('<a href="{}">{}</a>', value, value)
elif isinstance(field, models.JSONField) and value:
try:
Expand Down
14 changes: 14 additions & 0 deletions django/contrib/gis/db/backends/mysql/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,17 @@ def supports_geometry_field_unique_index(self):
# Not supported in MySQL since
# https://dev.mysql.com/worklog/task/?id=11808
return self.connection.mysql_is_mariadb

@cached_property
def django_test_skips(self):
skips = super().django_test_skips
if self.connection.mysql_is_mariadb:
skips.update(
{
"MariaDB doesn't support nested geometry collections.": {
"gis_tests.geoapp.tests.SaveLoadTests."
"test_geometrycollectionfield_default_max_ignored_on_read",
},
}
)
return skips
4 changes: 3 additions & 1 deletion django/contrib/gis/db/backends/mysql/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ def get_geometry_converter(self, expression):

def converter(value, expression, connection):
if value is not None:
geom = GEOSGeometryBase(read(memoryview(value)), geom_class)
geom = GEOSGeometryBase(
read(memoryview(value), max_geom_collections=None), geom_class
)
if srid:
geom.srid = srid
return geom
Expand Down
4 changes: 4 additions & 0 deletions django/contrib/gis/db/backends/oracle/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ def django_test_skips(self):
"gis_tests.gis_migrations.test_operations.OperationTests."
"test_add_check_constraint",
},
"Oracle doesn't support nested geometry collections.": {
"gis_tests.geoapp.tests.SaveLoadTests."
"test_geometrycollectionfield_default_max_ignored_on_read",
},
}
)
return skips
5 changes: 4 additions & 1 deletion django/contrib/gis/db/backends/oracle/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@ def get_geometry_converter(self, expression):

def converter(value, expression, connection):
if value is not None:
geom = GEOSGeometryBase(read(memoryview(value.read())), geom_class)
geom = GEOSGeometryBase(
read(memoryview(value.read()), max_geom_collections=None),
geom_class,
)
if srid:
geom.srid = srid
return geom
Expand Down
9 changes: 6 additions & 3 deletions django/contrib/gis/db/backends/postgis/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,12 @@ def get_geometry_converter(self, expression):
geom_class = expression.output_field.geom_class

def converter(value, expression, connection):
if isinstance(value, str): # Coming from hex strings.
value = value.encode("ascii")
return None if value is None else GEOSGeometryBase(read(value), geom_class)
if value is not None:
if isinstance(value, str): # Coming from hex strings.
value = value.encode("ascii")
return GEOSGeometryBase(
read(value, max_geom_collections=None), geom_class
)

return converter

Expand Down
5 changes: 4 additions & 1 deletion django/contrib/gis/db/backends/spatialite/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,9 @@ def get_geometry_converter(self, expression):
read = wkb_r().read

def converter(value, expression, connection):
return None if value is None else GEOSGeometryBase(read(value), geom_class)
if value is not None:
return GEOSGeometryBase(
read(value, max_geom_collections=None), geom_class
)

return converter
55 changes: 43 additions & 12 deletions django/contrib/gis/db/models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from django.contrib.gis import forms, gdal
from django.contrib.gis.db.models.proxy import SpatialProxy
from django.contrib.gis.gdal.error import GDALException
from django.contrib.gis.gdal.raster.const import VSI_FILESYSTEM_PREFIX
from django.contrib.gis.gdal.raster.source import DisallowedRasterLookup
from django.contrib.gis.geometry import json_regex
from django.contrib.gis.geos import (
GeometryCollection,
GEOSException,
Expand All @@ -14,6 +17,7 @@
Point,
Polygon,
)
from django.contrib.gis.geos.prototypes.io import MAX_GEOM_COLLECTIONS
from django.core.exceptions import ImproperlyConfigured
from django.db.models import Field
from django.utils.translation import gettext_lazy as _
Expand Down Expand Up @@ -170,21 +174,19 @@ def get_db_prep_value(self, value, connection, *args, **kwargs):
def get_raster_prep_value(self, value, is_candidate):
"""
Return a GDALRaster if conversion is successful, otherwise return None.

Unless the user opts in by wrapping values in a GDALRaster, raise
DisallowedRasterLookup for values that fetch or write to disk.
"""
if isinstance(value, gdal.GDALRaster):
return value
elif is_candidate:
gdal.GDALRaster.check_raster_lookup_value(value)
if is_candidate:
try:
return gdal.GDALRaster(value)
except GDALException:
pass
elif isinstance(value, dict):
try:
return gdal.GDALRaster(value)
except GDALException:
raise ValueError(
"Couldn't create spatial object from lookup value '%s'." % value
)
return None

def get_prep_value(self, value):
obj = super().get_prep_value(value)
Expand All @@ -201,22 +203,39 @@ def get_prep_value(self, value):
obj, "__geo_interface__"
)
# Try to convert the input to raster.
raster = self.get_raster_prep_value(obj, is_candidate)

raster = None
blocked_err = None
try:
raster = self.get_raster_prep_value(obj, is_candidate)
except DisallowedRasterLookup as err:
if isinstance(obj, dict):
raise err
# Don't immediately raise in case this is a valid GEOSGeometry.
blocked_err = err
if raster:
obj = raster
elif is_candidate:
max_geom_collections = getattr(
self, "max_geom_collections", MAX_GEOM_COLLECTIONS
)
try:
obj = GEOSGeometry(obj)
obj = GEOSGeometry(obj, max_geom_collections=max_geom_collections)
except (TypeError, ValueError) as err:
if isinstance(obj, str) and obj.startswith(VSI_FILESYSTEM_PREFIX):
raise blocked_err
raise err
except (GEOSException, GDALException):
if isinstance(obj, str) and json_regex.match(obj):
raise blocked_err
raise ValueError(
"Couldn't create spatial object from lookup value '%s'." % obj
)
else:
raise ValueError(
msg = (
"Cannot use object with type %s for a spatial lookup parameter."
% type(obj).__name__
)
raise blocked_err or ValueError(msg)

# Assigning the SRID value.
obj.srid = self.get_srid(obj)
Expand Down Expand Up @@ -244,6 +263,7 @@ def __init__(
*,
extent=(-180.0, -90.0, 180.0, 90.0),
tolerance=0.05,
max_geom_collections=MAX_GEOM_COLLECTIONS,
**kwargs,
):
"""
Expand All @@ -262,6 +282,10 @@ def __init__(
tolerance:
Define the tolerance, in meters, to use for the geometry field
entry in the `USER_SDO_GEOM_METADATA` table. Defaults to 0.05.

max_geom_collections:
The maximum number of geometry collections accepted before parsing is
refused, forwarded to the form field.
"""
# Setting the dimension of the geometry field.
self.dim = dim
Expand All @@ -274,6 +298,10 @@ def __init__(
self._extent = extent
self._tolerance = tolerance

# Limit on nested/total geometry collections, forwarded to the form
# field to guard against crashes in GEOS from deeply nested input.
self.max_geom_collections = max_geom_collections

super().__init__(verbose_name=verbose_name, **kwargs)

def deconstruct(self):
Expand All @@ -287,6 +315,8 @@ def deconstruct(self):
kwargs["extent"] = self._extent
if self._tolerance != 0.05:
kwargs["tolerance"] = self._tolerance
if self.max_geom_collections != MAX_GEOM_COLLECTIONS:
kwargs["max_geom_collections"] = self.max_geom_collections
return name, path, args, kwargs

def contribute_to_class(self, cls, name, **kwargs):
Expand All @@ -304,6 +334,7 @@ def formfield(self, **kwargs):
"form_class": self.form_class,
"geom_type": self.geom_type,
"srid": self.srid,
"max_geom_collections": self.max_geom_collections,
**kwargs,
}
if self.dim > 2 and not getattr(
Expand Down
7 changes: 6 additions & 1 deletion django/contrib/gis/db/models/proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,12 @@ def __get__(self, instance, cls=None):
else:
# Otherwise, a geometry or raster object is built using the field's
# contents, and the model's corresponding attribute is set.
geo_obj = self._load_func(geo_value)
try:
max_geoms = self.field.max_geom_collections
except AttributeError:
geo_obj = self._load_func(geo_value)
else:
geo_obj = self._load_func(geo_value, max_geom_collections=max_geoms)
setattr(instance, self.field.attname, geo_obj)
return geo_obj

Expand Down
16 changes: 14 additions & 2 deletions django/contrib/gis/forms/fields.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django import forms
from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.contrib.gis.geos.prototypes.io import MAX_GEOM_COLLECTIONS
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _

Expand All @@ -15,6 +16,7 @@ class GeometryField(forms.Field):

widget = OpenLayersWidget
geom_type = "GEOMETRY"
max_geom_collections = MAX_GEOM_COLLECTIONS

default_error_messages = {
"required": _("No geometry value provided."),
Expand All @@ -26,12 +28,20 @@ class GeometryField(forms.Field):
),
}

def __init__(self, *, srid=None, geom_type=None, **kwargs):
def __init__(
self, *, srid=None, geom_type=None, max_geom_collections=None, **kwargs
):
self.srid = srid
if geom_type is not None:
self.geom_type = geom_type
if max_geom_collections is not None:
self.max_geom_collections = max_geom_collections
super().__init__(**kwargs)
self.widget.attrs["geom_type"] = self.geom_type
# Propagate the limit to the (per-field) widget instance, which does
# the actual parsing. Custom widgets that override deserialize() and
# ignore this attribute still get the default limit via GEOSGeometry.
self.widget.max_geom_collections = self.max_geom_collections

def to_python(self, value):
"""Transform the value to a Geometry object."""
Expand All @@ -43,7 +53,9 @@ def to_python(self, value):
value = self.widget.deserialize(value)
else:
try:
value = GEOSGeometry(value)
value = GEOSGeometry(
value, max_geom_collections=self.max_geom_collections
)
except (GEOSException, ValueError, TypeError):
value = None
if value is None:
Expand Down
4 changes: 3 additions & 1 deletion django/contrib/gis/forms/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from django.contrib.gis.gdal import GDALException
from django.contrib.gis.geometry import json_regex
from django.contrib.gis.geos import GEOSException, GEOSGeometry
from django.contrib.gis.geos.prototypes.io import MAX_GEOM_COLLECTIONS
from django.forms.widgets import Widget

logger = logging.getLogger("django.contrib.gis")
Expand All @@ -19,6 +20,7 @@ class BaseGeometryWidget(Widget):
geom_type = "GEOMETRY"
map_srid = 4326
display_raw = False
max_geom_collections = MAX_GEOM_COLLECTIONS

supports_3d = False
template_name = "" # set on subclasses
Expand All @@ -36,7 +38,7 @@ def serialize(self, value):

def deserialize(self, value):
try:
return GEOSGeometry(value)
return GEOSGeometry(value, max_geom_collections=self.max_geom_collections)
except (GEOSException, GDALException, ValueError, TypeError) as err:
logger.error("Error creating geometry from value '%s' (%s)", value, err)
return None
Expand Down
Loading
Loading