Example #1
0
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)
Example #2
0
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)
Example #3
0
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,
            ))
Example #4
0
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
Example #5
0
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}')",
                ))