def _parse_namespaces(self, value) -> dict[str, str]: """Parse the namespaces definition. The NAMESPACES parameter defines which namespaces are used in the KVP request. When this parameter is not given, the default namespaces are assumed. """ if not value: return {} # example single value: xmlns(http://example.org) # or: namespaces=xmlns(xml,http://www.w3.org/...),xmlns(wfs,http://www.opengis.net/...) tokens = value.split(",") namespaces = {} tokens = iter(tokens) for prefix in tokens: if not prefix.startswith("xmlns("): raise InvalidParameterValue( "namespaces", f"Expected xmlns(...) format: {value}") if prefix.endswith(")"): # xmlns(http://...) prefix = "" uri = prefix[6:-1] else: uri = next(tokens, "") if not uri.endswith(")"): raise InvalidParameterValue( "namespaces", f"Expected xmlns(prefix,uri) format: {value}") prefix = prefix[6:] uri = uri[:-1] namespaces[uri] = prefix return namespaces
def from_kvp_request(cls, **params): """Build this object from a HTTP GET (key-value-pair) request.""" # Validate optionally required parameters if not params["typeNames"] and not params["resourceID"]: raise MissingParameterValue("typeNames", "Empty TYPENAMES parameter") # Validate mutually exclusive parameters if params["filter"] and (params["bbox"] or params["resourceID"]): raise InvalidParameterValue( "filter", "The FILTER parameter is mutually exclusive with BBOX and RESOURCEID", ) # Validate mutually exclusive parameters if params["resourceID"]: if params["bbox"] or params["filter"]: raise InvalidParameterValue( "resourceID", "The RESOURCEID parameter is mutually exclusive with BBOX and FILTER", ) # When ResourceId + typenames is defined, it should be a value from typenames # see WFS spec 7.9.2.4.1 if params["typeNames"]: id_type_names = params["resourceID"].type_names if id_type_names: # Only test when the RESOURCEID has a typename.id format # Otherwise, this breaks the CITE RESOURCEID=test-UUID parameter. kvp_type_names = { feature_type.name for feature_type in params["typeNames"] } if not kvp_type_names.issuperset(id_type_names): raise InvalidParameterValue( "resourceID", "When TYPENAMES and RESOURCEID are combined, " "the RESOURCEID type should be included in TYPENAMES.", ) return AdhocQuery( typeNames=params["typeNames"], filter=params["filter"], filter_language=params["filter_language"], bbox=params["bbox"], sortBy=params["sortBy"], resourceId=params["resourceID"], value_reference=params.get("valueReference"), )
def resolve_function(self, function_name) -> FesFunction: """Resole the function using it's name.""" try: return self.functions[function_name] except KeyError: raise InvalidParameterValue( "filter", f"Unsupported function: {function_name}") from None
def get_queryset(self, feature_type: FeatureType) -> QuerySet: """Generate the queryset for the specific feature type. This method can be overwritten in subclasses to define the returned data. However, consider overwriting :meth:`compile_query` instead of simple data. """ queryset = feature_type.get_queryset() # Apply filters compiler = self.compile_query(feature_type, using=queryset.db) if self.value_reference is not None: if feature_type.resolve_element( self.value_reference.xpath) is None: raise InvalidParameterValue( "valueReference", f"Field '{self.value_reference.xpath}' does not exist.", ) # For GetPropertyValue, adjust the query so only that value is requested. # This makes sure XPath attribute selectors are already handled by the # database query, instead of being a presentation-layer handling. field = compiler.add_value_reference(self.value_reference) queryset = compiler.filter_queryset(queryset, feature_type=feature_type) return queryset.values("pk", member=field) else: return compiler.filter_queryset(queryset, feature_type=feature_type)
def get_operation_class(self) -> type[base.WFSMethod]: """Resolve the method that the client wants to call.""" if not self.accept_operations: raise ImproperlyConfigured("View has no operations") # The service is always WFS service = self._get_required_arg("SERVICE", self.default_service).upper() try: operations = self.accept_operations[service] except KeyError: allowed = ", ".join(sorted(self.accept_operations.keys())) raise InvalidParameterValue( "service", f"'{service}' is an invalid service, supported are: {allowed}.", ) from None # Resolve the operation # In mapserver, the operation name is case insensitive. operation = self._get_required_arg("REQUEST").upper() uc_methods = { name.upper(): method for name, method in operations.items() } try: return uc_methods[operation] except KeyError: allowed = ", ".join(operations.keys()) raise OperationNotSupported( "request", f"'{operation.lower()}' is not implemented, supported are: {allowed}.", ) from None
def _parse_accept_versions(self, accept_versions) -> str: """Special parsing for the ACCEPTVERSIONS parameter.""" if "VERSION" in self.view.KVP: # Even GetCapabilities can still receive a VERSION argument to fixate it. raise InvalidParameterValue( "AcceptVersions", "Can't provide both ACCEPTVERSIONS and VERSION" ) matched_versions = set(accept_versions.split(",")).intersection( self.view.accept_versions ) if not matched_versions: allowed = ", ".join(self.view.accept_versions) raise VersionNegotiationFailed( "acceptversions", f"'{accept_versions}' does not contain supported versions, " f"supported are: {allowed}.", ) # Take the highest version (mapserver returns first matching) requested_version = sorted(matched_versions, reverse=True)[0] # Make sure the views+exceptions continue to operate in this version self.view.set_version(requested_version) return requested_version
def value_from_query(self, KVP: dict): kvp_name = self.name.upper() if kvp_name in KVP: raise InvalidParameterValue( self.name, f"Support for {self.name} is not implemented!" ) return None
def resolve_query(self, query_id) -> type[StoredQuery]: """Find the stored procedure using the ID.""" try: return self.stored_queries[query_id] except KeyError: raise InvalidParameterValue( "STOREDQUERY_ID", f"Stored query does not exist: {query_id}") from None
def validate_value(self, value): """Validate the parsed value. This method can be overwritten by a subclass if needed. """ if self.allowed_values is not None and value not in self.allowed_values: msg = self.error_messages.get("invalid", "Invalid value for {name}: {value}") raise InvalidParameterValue( self.name, msg.format(name=self.name, value=value))
def validate(cls, feature_type: FeatureType, **params): """Validate the presentation parameters""" crs = params["srsName"] if (conf.GISSERVER_SUPPORTED_CRS_ONLY and crs is not None and crs not in feature_type.supported_crs): raise InvalidParameterValue( "srsName", f"Feature '{feature_type.name}' does not support SRID {crs.srid}.", )
def _parse_output_format(self, value) -> OutputFormat: """Select the proper OutputFormat object based on the input value""" value = value.replace(" ", "+") # allow application/gml+xml on the KVP. try: return next(o for o in self.output_formats if o.matches(value)) except StopIteration: raise InvalidParameterValue( "outputformat", f"'{value}' is not a permitted output format for this operation.", ) from None
def value_from_query(self, KVP: dict): # noqa: C901 """Parse a request variable using the type definition. This uses the dataclass settings to parse the incoming request value. """ # URL-based key-value-pair parameters use uppercase. kvp_name = self.name.upper() value = KVP.get(kvp_name) if not value and self.alias: value = KVP.get(self.alias.upper()) # Check required field settings, both empty and missing value are treated the same. if not value: if not self.required: return self.default elif value is None: raise MissingParameterValue(self.name, f"Missing {kvp_name} parameter") else: raise InvalidParameterValue(self.name, f"Empty {kvp_name} parameter") # Allow conversion into a python object if self.parser is not None: try: value = self.parser(value) except ExternalParsingError as e: raise OperationParsingFailed( self.name, f"Unable to parse {kvp_name} argument: {e}") from None except (TypeError, ValueError, NotImplementedError) as e: # TypeError/ValueError are raised by most handlers for unexpected data # The NotImplementedError can be raised by fes parsing. raise InvalidParameterValue( self.name, f"Invalid {kvp_name} argument: {e}") from None # Validate against value choices self.validate_value(value) return value
def resolve_type_name(self, type_name, locator="typename") -> FeatureType: """Find the feature type for a given name. This is an utility that cusstom subclasses can use. """ # Strip the namespace prefix. The Python ElementTree parser does # not expose the used namespace prefixes, so text-values can't be # mapped against it. As we expose just one namespace, just strip it. xmlns, type_name = split_xml_name(type_name) try: return self.all_feature_types[type_name] except KeyError: raise InvalidParameterValue( locator, f"Typename '{type_name}' doesn't exist in this server." ) from None
def _parse_type_name(self, name, locator="typename") -> FeatureType: """Find the requested feature type for a type name""" app_prefix = self.namespaces[self.view.xml_namespace] if name.startswith(f"{app_prefix}:"): local_name = name[len(app_prefix) + 1:] # strip our XML prefix else: local_name = name try: return self.all_feature_types_by_name[local_name] except KeyError: raise InvalidParameterValue( locator, f"Typename '{name}' doesn't exist in this server. " f"Please check the capabilities and reformulate your request.", ) from None
def from_string(cls, value: str): """Construct the SortBy object from a KVP "SORTBY" parameter.""" props = [] for field in value.split(","): if "[" in field: raise InvalidParameterValue( "sortby", "Sorting with XPath attribute selectors is not supported.") if " " in field: xpath, direction = field.split(" ", 1) props.append( SortProperty( value_reference=ValueReference(xpath), sort_order=SortOrder.from_string(direction), )) else: props.append( SortProperty(value_reference=ValueReference(field))) return cls(sort_properties=props)
def extract_parameters(cls, KVP) -> dict[str, str]: """Extract the arguments from the key-value-pair (=HTTP GET) request.""" args = {} for name, _xsd_type in cls.meta.parameters.items(): try: args[name] = KVP[name] except KeyError: raise MissingParameterValue( name, f"Stored query {cls.meta.id} requires an '{name}' parameter" ) from None # Avoid unexpected behavior, check whether the client also sends adhoc query parameters for name in ("filter", "bbox", "resourceID"): if name not in args and KVP.get(name.upper()): raise InvalidParameterValue( name, "Stored query can't be combined with adhoc-query parameters" ) return args
def _compile_non_filter_query(self, feature_type: FeatureType, using=None): """Generate the query based on the remaining parameters. This is slightly more efficient then generating the fes Filter object from these KVP parameters (which could also be done within the request method). """ compiler = fes20.CompiledQuery(feature_type=feature_type, using=using) if self.bbox: # Validate whether the provided SRID is supported. # While PostGIS would support many more ID's, # it would crash when an unsupported ID is given. crs = self.bbox.crs if (conf.GISSERVER_SUPPORTED_CRS_ONLY and crs is not None and crs not in feature_type.supported_crs): raise InvalidParameterValue( "bbox", f"Feature '{feature_type.name}' does not support SRID {crs.srid}.", ) # Using __within does not work with geometries # that only partially exist within the bbox lookup = operators.SpatialOperatorName.BBOX.value # "intersects" filters = { f"{feature_type.geometry_field.name}__{lookup}": self.bbox.as_polygon(), } compiler.add_lookups(Q(**filters)) if self.resourceId: self.resourceId.build_query(compiler=compiler) if self.sortBy: compiler.add_sort_by(self.sortBy) return compiler
def get_context_data(self, resultType, **params): # noqa: C901 query = self.get_query(**params) query.check_permissions(self.view.request) try: if resultType == "HITS": collection = self.get_hits(query) elif resultType == "RESULTS": # Validate StandardPresentationParameters collection = self.get_paginated_results(query, **params) else: raise NotImplementedError() except ExternalParsingError as e: # Bad input data self._log_filter_error(query, logging.ERROR, e) raise OperationParsingFailed(self._get_locator(**params), str(e)) from e except ExternalValueError as e: # Bad input data self._log_filter_error(query, logging.ERROR, e) raise InvalidParameterValue(self._get_locator(**params), str(e)) from e except ValidationError as e: # Bad input data self._log_filter_error(query, logging.ERROR, e) raise OperationParsingFailed( self._get_locator(**params), "\n".join(map(str, e.messages)) ) from e except FieldError as e: # e.g. doing a LIKE on a foreign key, or requesting an unknown field. if not conf.GISSERVER_WRAP_FILTER_DB_ERRORS: raise self._log_filter_error(query, logging.ERROR, e) raise InvalidParameterValue( self._get_locator(**params), "Internal error when processing filter" ) from e except (InternalError, ProgrammingError) as e: # e.g. comparing datetime against integer if not conf.GISSERVER_WRAP_FILTER_DB_ERRORS: raise logger.error("WFS request failed: %s\nParams: %r", e, params) msg = str(e) locator = ( "srsName" if "Cannot find SRID" in msg else self._get_locator(**params) ) raise InvalidParameterValue(locator, f"Invalid request: {msg}") from e except (TypeError, ValueError) as e: # TypeError/ValueError could reference a datatype mismatch in an # ORM query, but it could also be an internal bug. In most cases, # this is already caught by XsdElement.validate_comparison(). if self._is_orm_error(e): raise InvalidParameterValue( self._get_locator(**params), f"Invalid filter query: {e}" ) from e raise output_crs = params["srsName"] if not output_crs and collection.results: output_crs = collection.results[0].feature_type.crs # These become init kwargs for the selected OutputRenderer class: return { "source_query": query, "collection": collection, "output_crs": output_crs, }
def get_queryset(self, feature_type: FeatureType) -> QuerySet: """Override to implement ID type checking.""" try: return super().get_queryset(feature_type) except (ValueError, TypeError) as e: raise InvalidParameterValue("ID", f"Invalid ID value: {e}") from e
def get_feature_types(self) -> list[FeatureType]: """Generate map feature layers for all models that have geometry data.""" typenames = self.KVP.get("TYPENAMES") if not typenames: # Need to parse all models (e.g. GetCapabilities) subset = self.models else: # Already filter the number of exported features, to avoid intense database queries. # The dash is used as variants of the same feature. The xmlns: prefix is also removed # since it can differ depending on the NAMESPACES parameter. requested_names = { RE_SIMPLE_NAME.sub(r"\g<name>", name) for name in typenames.split(",") } subset = { name: model for name, model in self.models.items() if name in requested_names } if not subset: raise InvalidParameterValue( "typename", f"Typename '{typenames}' doesn't exist in this server. " f"Please check the capabilities and reformulate your request.", ) from None features = [] for model in subset.values(): geo_fields = self._get_geometry_fields(model) base_name = model._meta.model_name base_title = model._meta.verbose_name # When there are multiple geometry fields for a model, # multiple features are generated so both can be displayed. # By default, QGis and friends only show the first geometry. for i, geo_field in enumerate(geo_fields): # the get_unauthorized_fields() part of get_feature_fields() is an # expensive operation, hence this is only read when needed. fields = LazyList(self.get_feature_fields, model, geo_field.name) if i == 0: name = base_name title = base_title else: name = f"{base_name}-{geo_field.name}" title = f"{base_title} ({geo_field.verbose_name})" feature = AuthenticatedFeatureType( model, name=name, title=title, fields=fields, display_field_name=model.get_display_field(), geometry_field_name=geo_field.name, crs=crs.DEFAULT_CRS, other_crs=crs.OTHER_CRS, wfs_view=self, show_name_field=model.has_display_field(), ) features.append(feature) return features
def from_string(cls, direction): try: return cls[direction] except KeyError: raise InvalidParameterValue( "sortby", "Expect ASC/DESC ordering direction") from None