def wms_capabilities(request: SubRequest) -> wmshelper.WMSCapabilities: wms_version: str = request.param # type: ignore data_path = os.path.join( os.path.dirname(os.path.realpath(__file__)), "data", "wms", f"capabilities_{wms_version.replace('.', '_')}.xml") with open(data_path) as f: xml = f.read() return wmshelper.WMSCapabilities(xml)
def test_wms_exceptions(wms_version: str): """Test if ServiceExceptionReport's are parsed correctly""" data_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "data", "wms", f"exception_{wms_version.replace('.', '_')}.xml") with open(data_path) as f: xml = f.read() with pytest.raises(wmshelper.ServiceExceptionError): wmshelper.WMSCapabilities(xml)
def check_wms_endpoint(source: Dict[str, Any], messages: List[Message]) -> None: """Check WMS Endpoint source Parameters ---------- source : Dict[str, Any] The source messages : List[Message] The list to add messages to """ url = source["properties"]["url"] wms_url = wmshelper.WMSURL(url) try: validators.url(url) # type: ignore except validators.utils.ValidationFailure as e: messages.append( Message(level=MessageLevel.ERROR, message=f"URL validation error: {e} for {url}")) source_headers = get_http_headers(source) exceptions: List[str] = [] wms = None for wms_version in [None, "1.3.0", "1.1.1", "1.1.0", "1.0.0"]: if wms_version is None: wms_version_str = "-" else: wms_version_str = wms_version wms_getcapabilities_url = None try: wms_getcapabilities_url = wms_url.get_capabilities_url( wms_version=wms_version) _, xml = get_text_encoded(wms_getcapabilities_url, headers=source_headers) if xml is not None: wms = wmshelper.WMSCapabilities(xml) break except Exception as e: exceptions.append( f"WMS {wms_version_str}: Error: {e} {wms_getcapabilities_url}") continue # Check if it was possible to parse the WMS GetCapability response if wms is None: for msg in exceptions: messages.append(Message( level=MessageLevel.ERROR, message=msg, ))
async def update_wms(url: str, session: ClientSession, messages: List[str]) -> Optional[Tuple[str, Set[str]]]: """Update wms parameters using WMS GetCapabilities request Parameters ---------- wms_url : str session : ClientSession messages : list Returns ------- dict Dict with new url and available projections """ wms_url = wmshelper.WMSURL(url) layers = wms_url.layers() messages.append(f"Advertised layers: {','.join(layers)} for url {url}") old_wms_version = wms_url.wms_version() if old_wms_version is None: old_wms_version = "1.3.0" # Fetch highest supported WMS GetCapabilities wms_capabilities = None wms_getcapabilities_url = None for wms_version in supported_wms_versions: # Do not try versions older than in the url if wms_version < old_wms_version: continue try: wms_getcapabilities_url = wms_url.get_capabilities_url( wms_version=wms_version) messages.append(f"WMS url: {wms_getcapabilities_url}") resp = await get_url(wms_getcapabilities_url, session) if not resp.status == 200 or resp.text is None: messages.append( f"Could not download GetCapabilities URL for {wms_getcapabilities_url}: HTTP Code: {resp.status} {resp.exception}" ) continue xml = resp.text wms_capabilities = wmshelper.WMSCapabilities(xml) if wms_capabilities is not None: break except Exception as e: messages.append( f"Could not get GetCapabilities URL for {wms_version}: {e}") if wms_capabilities is None: # Could not request GetCapabilities messages.append("Could not contact WMS server. Check logs.") return None # Abort if a layer is not advertised by WMS server # If layer name exists but case does not match update to layer name advertised by server layers_advertised = wms_capabilities.layers layers_advertised_lower_case = { layer.lower(): layer for layer in layers_advertised } updated_layers: List[str] = [] for layer in layers: layer_lower = layer.lower() if layer_lower not in layers_advertised_lower_case: messages.append( f"Layer {layer} not advertised by server {wms_getcapabilities_url}" ) return None else: updated_layers.append(layers_advertised_lower_case[layer_lower]) # Use jpeg if format was not specified. But this should not happen in the first place. format = wms_url.format() if format is None: format = "image/jpeg" transparent = wms_url.is_transparent() # Keep transparent if format is png or gif, remove otherwise if transparent and not ("png" in format.lower() or "gif" in format.lower()): transparent = None new_url = wms_url.get_map_url( version=wms_capabilities.version, layers=updated_layers, styles=wms_url.styles(), crs="{proj}", bounds="{bbox}", format=format, width="{width}", height="{height}", transparent=transparent, ) # Each layer can support different projections. GetMap queries among different layers are only possible # for projections supported by all layers new_projections = wms_capabilities.layers[updated_layers[0]].crs for layer in updated_layers[1:]: new_projections.intersection_update(wms_capabilities.layers[layer].crs) layer_bbox = [ wms_capabilities.layers[layer].bbox for layer in updated_layers ] layer_bbox = [layer for layer in layer_bbox if layer is not None] # Some server report invalid projections. # Remove invalid projections or projections used outside of the area of the layers new_projections = eliutils.clean_projections(new_projections, layer_bbox) if len(new_projections) == 0: if len(updated_layers) > 1: messages.append("No common valid projections among layers.") else: messages.append("No valid projections for layer.") return None return new_url, new_projections
def check_wms(source: Dict[str, Any], messages: List[Message]) -> None: """Check WMS source Parameters ---------- source : Dict[str, Any] The source messages : List[Message] The list to add messages to """ url = source["properties"]["url"] wms_url = wmshelper.WMSURL(url) source_headers = get_http_headers(source) params = ["{proj}", "{bbox}", "{width}", "{height}"] missingparams = [p for p in params if p not in url] if len(missingparams) > 0: messages.append( Message( level=MessageLevel.ERROR, message= f"The following values are missing in the URL: {','.join(missingparams)}", )) try: wms_url.is_valid_getmap_url() except validators.utils.ValidationFailure as e: messages.append( Message(level=MessageLevel.ERROR, message=f"URL validation error {e} for {url}")) # Check mandatory WMS GetMap parameters (Table 8, Section 7.3.2, WMS 1.3.0 specification) # Normalize parameter names to lower case wms_args = {key.lower(): value for key, value in wms_url.get_parameters()} # Check if it is actually a ESRI Rest url and not a WMS url is_esri = "request" not in wms_args # Check if required parameters are missing missing_request_parameters: Set[str] = set() if is_esri: required_parameters = [ "f", "bbox", "size", "imageSR", "bboxSR", "format" ] else: required_parameters = [ "version", "request", "layers", "bbox", "width", "height", "format", ] for request_parameter in required_parameters: if request_parameter.lower() not in wms_args: missing_request_parameters.add(request_parameter) if not is_esri: if "version" in wms_args and wms_args["version"] == "1.3.0": if "crs" not in wms_args: missing_request_parameters.add("crs") if "srs" in wms_args: messages.append( Message( level=MessageLevel.ERROR, message= f"WMS {wms_args['version']} URLs should not contain SRS parameter: {url}", )) elif "version" in wms_args and not wms_args["version"] == "1.3.0": if "srs" not in wms_args: missing_request_parameters.add("srs") if "crs" in wms_args: messages.append( Message( level=MessageLevel.ERROR, message= f"WMS {wms_args['version']} URLs should not contain CRS parameter: {url}", )) if len(missing_request_parameters) > 0: missing_request_parameters_str = ",".join(missing_request_parameters) messages.append( Message( level=MessageLevel.ERROR, message= f"Parameter '{missing_request_parameters_str}' is missing in URL: {url}.", )) return # Nothing more to do for ESRI Rest API if is_esri: return # Styles is mandatory according to the WMS specification, but some WMS servers seems not to care if "styles" not in wms_args: messages.append( Message( level=MessageLevel.WARNING, message= f"Parameter 'styles' is missing in url. 'STYLES=' can be used to request default style.: {url}", )) # We first send a service=WMS&request=GetCapabilities request to server # According to the WMS Specification Section 6.2 Version numbering and negotiation, the server should return # the GetCapabilities XML with the highest version the server supports. # If this fails, it is tried to explicitly specify a WMS version exceptions: List[str] = [] wms = None for wms_version in [None, "1.3.0", "1.1.1", "1.1.0", "1.0.0"]: if wms_version is None: wms_version_str = "-" else: wms_version_str = wms_version wms_getcapabilities_url = None try: wms_getcapabilities_url = wms_url.get_capabilities_url( wms_version=wms_version) _, xml = get_text_encoded(wms_getcapabilities_url, headers=source_headers) if xml is not None: wms = wmshelper.WMSCapabilities(xml) break except Exception as e: exceptions.append( f"WMS {wms_version_str}: Error: {e} {wms_getcapabilities_url}") continue # Check if it was possible to parse the WMS GetCapability response # If not, there is nothing left to check if wms is None: for msg in exceptions: messages.append(Message( level=MessageLevel.ERROR, message=msg, )) return # Log access constraints and fees metadata for access_constraint in wms.access_constraints: messages.append( Message( level=MessageLevel.INFO, message=f"AccessConstraints: {access_constraint}", )) for fee in wms.fees: messages.append( Message( level=MessageLevel.INFO, message=f"Fee: {fee}", )) if source["geometry"] is None: geom = None else: geom = shape(source["geometry"]) # type: ignore # Check layers if "layers" in wms_args: layers = wms_args["layers"].split(",") # Check if layers in WMS GetMap URL are advertised by WMS server. not_found_layers = [ layer_name for layer_name in layers if layer_name not in wms.layers ] if len(not_found_layers) > 0: messages.append( Message( level=MessageLevel.ERROR, message= f"Layers '{','.join(not_found_layers)}' not advertised by WMS GetCapabilities request (Some server do not advertise layers, but they are very rare).: {url}", )) # Check source geometry against layer bounding box # Regardless of its projection, each layer should advertise an approximated bounding box in lon/lat. # See WMS 1.3.0 Specification Section 7.2.4.6.6 EX_GeographicBoundingBox if geom is not None and geom.is_valid: # type: ignore bboxs = [ wms.layers[layer_name].bbox for layer_name in layers if layer_name in wms.layers and wms.layers[layer_name].bbox ] bboxs = [bbox for bbox in bboxs if bbox is not None] max_area_outside = max_area_outside_bbox(geom, bboxs) # type: ignore # 5% is an arbitrary chosen value and should be adapted as needed if max_area_outside > 5.0: messages.append( Message( level=MessageLevel.ERROR, message= f"{round(max_area_outside, 2)}% of geometry is outside of the layers bounding box. Geometry should be checked", )) # Check styles if "styles" in wms_args: style_parameter = wms_args["styles"] # default style needs not to be advertised by the server if not (style_parameter == "default" or style_parameter == "" or style_parameter == "," * len(layers)): styles = style_parameter.split(",") if not len(styles) == len(layers): messages.append( Message( level=MessageLevel.ERROR, message= f"Not the same number of styles and layers. {len(styles)} vs {len(layers)}", )) else: for layer_name, style_name in zip(layers, styles): if (len(style_name) > 0 and not style_name == "default" and layer_name in wms.layers and style_name not in wms.layers[layer_name].styles): messages.append( Message( level=MessageLevel.ERROR, message= f"Layer '{layer_name}' does not support style '{style_name}'", )) # Check CRS if "available_projections" not in source["properties"]: messages.append( Message( level=MessageLevel.ERROR, message= f"Sources of type wms must include the 'available_projections' element.", )) else: # A WMS server can include many CRS. Some of them are frequently used by editors. We require them to be included if they are supported by the WMS server. crs_should_included_if_available = { "EPSG:4326", "EPSG:3857", "CRS:84" } for layer_name in layers: if layer_name in wms.layers: # Check for CRS in available_projections that are not advertised by the WMS server not_supported_crs: Set[str] = set() available_projections: List[str] = source["properties"][ "available_projections"] for crs in available_projections: if crs.upper() not in wms.layers[layer_name].crs: not_supported_crs.add(crs) if len(not_supported_crs) > 0: supported_crs_str = ",".join( wms.layers[layer_name].crs) not_supported_crs_str = ",".join(not_supported_crs) messages.append( Message( level=MessageLevel.WARNING, message= f"Layer '{layer_name}': CRS '{not_supported_crs_str}' not in: {supported_crs_str}. Some server support CRS which are not advertised.", )) # Check for CRS supported by the WMS server but not in available_projections supported_but_not_included: Set[str] = set() for crs in crs_should_included_if_available: if crs not in available_projections and crs in wms.layers[ layer_name].crs: supported_but_not_included.add(crs) if len(supported_but_not_included) > 0: supported_but_not_included_str = ",".join( supported_but_not_included) messages.append( Message( level=MessageLevel.WARNING, message= f"Layer '{layer_name}': CRS '{supported_but_not_included_str}' not included in available_projections but supported by server.", )) # Check if server supports a newer WMS version as in url if wms_args["version"] < wms.version: messages.append( Message( level=MessageLevel.WARNING, message= f"Query requests WMS version '{wms_args['version']}', server supports '{wms.version}'", )) # Check image formats request_imagery_format = wms_args["format"] wms_advertised_formats_str = "', '".join(wms.formats) if request_imagery_format not in wms.formats: messages.append( Message( level=MessageLevel.ERROR, message= f"Format '{request_imagery_format}' not in '{wms_advertised_formats_str}': {url}.", )) # For photo sources it is recommended to use jpeg format, if it is available if "category" in source["properties"] and "photo" in source["properties"][ "category"]: if "jpeg" not in request_imagery_format and "jpeg" in wms.formats: messages.append( Message( level=MessageLevel.WARNING, message= f"Server supports JPEG, but '{request_imagery_format}' is used. " f"JPEG is typically preferred for photo sources, but might not be always " f"the best choice. (Server supports: '{wms_advertised_formats_str}')", ))