Exemple #1
0
def check_wms_endpoint(source, info_msgs, warning_msgs, error_msgs):
    """
    Check WMS Endpoint source

    Currently it is only tested if a GetCapabilities request can be parsed.

    Parameters
    ----------
    source : dict
        Source dictionary
    info_msgs : list
        Good messages
    warning_msgs: list
        Warning messages
    error_msgs: list:
        Error Messages

    """

    wms_url = source["properties"]["url"]
    source_headers = get_http_headers(source)
    if not validators.url(wms_url):
        error_msgs.append(f"URL validation error: {wms_url}")

    wms_args = {}
    u = urlparse(wms_url)
    for k, v in parse_qsl(u.query, keep_blank_values=True):
        wms_args[k.lower()] = v

    for wmsversion in [None, "1.3.0", "1.1.1", "1.1.0", "1.0.0"]:
        try:
            url = wmshelper.get_getcapabilities_url(wms_url,
                                                    wms_version=wmsversion)
            r = requests.get(url, headers=source_headers)
            xml = r.text
            wms = wmshelper.parse_wms(xml)

            for access_constraint in wms["AccessConstraints"]:
                info_msgs.append(f"AccessConstraints: {access_constraint}")
            for fee in wms["Fees"]:
                info_msgs.append(f"Fee: {fee}")

            break
        except Exception as e:
            error_msgs.append(f"WMS: {wmsversion} Exception: {e}")
Exemple #2
0
def check_wms(source, info_msgs, warning_msgs, error_msgs):
    """
    Check WMS source

    Parameters
    ----------
    source : dict
        Source dictionary
    info_msgs : list
        Good messages
    warning_msgs: list
        Warning messages
    error_msgs: list:
        Error Messages
    """

    wms_url = source["properties"]["url"]
    source_headers = get_http_headers(source)

    params = ["{proj}", "{bbox}", "{width}", "{height}"]
    missingparams = [p for p in params if p not in wms_url]
    if len(missingparams) > 0:
        error_msgs.append(
            f"The following values are missing in the URL: {','.join(missingparams)}"
        )

    wms_args = {}
    u = urlparse(wms_url)
    url_parts = list(u)
    for k, v in parse_qsl(u.query, keep_blank_values=True):
        wms_args[k.lower()] = v

    def validate_wms_getmap_url():
        """
        Layers and styles can contain whitespaces. Ignore them here. They are checked against GetCapabilities later.
        """
        url_parts_without_layers = "&".join([
            f"{key}={value}" for key, value in wms_args.items()
            if key not in {"layers", "styles"}
        ])
        parts = url_parts.copy()
        parts[4] = url_parts_without_layers
        url = urlunparse(parts).replace("{", "").replace("}", "")
        return validators.url(url)

    if not validate_wms_getmap_url():
        error_msgs.append(f"URL validation error: {wms_url}")

    # Check mandatory WMS GetMap parameters (Table 8, Section 7.3.2, WMS 1.3.0 specification)
    missing_request_parameters = set()
    is_esri = "request" not in wms_args
    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)

    # Nothing more to do for esri rest api
    if is_esri:
        return

    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:
            error_msgs.append(
                f"WMS {wms_args['version']} urls should not contain SRS parameter."
            )
    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:
            error_msgs.append(
                f"WMS {wms_args['version']} urls should not contain CRS parameter."
            )
    if len(missing_request_parameters) > 0:
        missing_request_parameters_str = ",".join(missing_request_parameters)
        error_msgs.append(
            f"Parameter '{missing_request_parameters_str}' is missing in url.")
        return
    # Styles is mandatory according to the WMS specification, but some WMS servers seems not to care
    if "styles" not in wms_args:
        warning_msgs.append(
            "Parameter 'styles' is missing in url. 'STYLES=' can be used to request default style."
        )

    # 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 = []
    wms = None
    for wmsversion in [None, "1.3.0", "1.1.1", "1.1.0", "1.0.0"]:
        if wmsversion is None:
            wmsversion_str = "-"
        else:
            wmsversion_str = wmsversion

        try:
            wms_getcapabilites_url = wmshelper.get_getcapabilities_url(
                wms_url, wmsversion)
            r = requests.get(wms_getcapabilites_url, headers=source_headers)
            xml = r.text
            wms = wmshelper.parse_wms(xml)
            if wms is not None:
                break
        except Exception as e:
            exceptions.append(f"WMS {wmsversion_str}: Error: {e}")
            continue

    if wms is None:
        for msg in exceptions:
            error_msgs.append(msg)
        return

    for access_constraint in wms["AccessConstraints"]:
        info_msgs.append(f"AccessConstraints: {access_constraint}")
    for fee in wms["Fees"]:
        info_msgs.append(f"Fee: {fee}")

    if source["geometry"] is None:
        geom = None
    else:
        geom = eliutils.parse_eli_geometry(source["geometry"])

    # Check layers
    if "layers" in wms_args:
        layer_arg = wms_args["layers"]
        layers = layer_arg.split(",")
        not_found_layers = []
        for layer_name in layer_arg.split(","):
            if layer_name not in wms["layers"]:
                not_found_layers.append(layer_name)
        if len(not_found_layers) > 0:
            error_msgs.append(
                f"Layers '{','.join(not_found_layers)}' not advertised by WMS GetCapabilities "
                "request.")

        # 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:
            max_outside = 0.0
            for layer_name in layers:
                if layer_name in wms["layers"]:
                    bbox = wms["layers"][layer_name]["BBOX"]
                    geom_bbox = box(*bbox)
                    geom_outside_bbox = geom.difference(geom_bbox)
                    area_outside_bbox = geom_outside_bbox.area / geom.area * 100.0
                    max_outside = max(max_outside, area_outside_bbox)
            # 5% is an arbitrary chosen value and should be adapted as needed
            if max_outside > 5.0:
                error_msgs.append(
                    f"{round(area_outside_bbox, 2)}% of geometry is outside of the layers bounding box. "
                    "Geometry should be checked")

        # Check styles
        if "styles" in wms_args:
            style = wms_args["styles"]
            # default style needs not to be advertised by the server
            if not (style == "default" or style == ""
                    or style == "," * len(layers)):
                styles = wms_args["styles"].split(",")
                if not len(styles) == len(layers):
                    error_msgs.append(
                        "Not the same number of styles and layers.")
                else:
                    for layer_name, style in zip(layers, styles):
                        if (len(style) > 0 and not style == "default"
                                and layer_name in wms["layers"] and style
                                not in wms["layers"][layer_name]["Styles"]):
                            error_msgs.append(
                                f"Layer '{layer_name}' does not support style '{style}'"
                            )

        # Check CRS
        crs_should_included_if_available = {"EPSG:4326", "EPSG:3857", "CRS:84"}
        if "available_projections" not in source["properties"]:
            error_msgs.append(
                "source is missing 'available_projections' element.")
        else:
            for layer_name in layers:
                if layer_name in wms["layers"]:
                    not_supported_crs = set()
                    for crs in source["properties"]["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)
                        warning_msgs.append(
                            f"Layer '{layer_name}': CRS '{not_supported_crs_str}' not in: {supported_crs_str}. Some server support CRS which are not advertised."
                        )

                    supported_but_not_included = set()
                    for crs in crs_should_included_if_available:
                        if (crs not in source["properties"]
                            ["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)
                        warning_msgs.append(
                            f"Layer '{layer_name}': CRS '{supported_but_not_included_str}' not included in available_projections but "
                            "supported by server.")

    if wms_args["version"] < wms["version"]:
        warning_msgs.append(
            f"Query requests WMS version '{wms_args['version']}', server supports '{wms['version']}'"
        )

    # Check formats
    imagery_format = wms_args["format"]
    imagery_formats_str = "', '".join(wms["formats"])
    if imagery_format not in wms["formats"]:
        error_msgs.append(
            f"Format '{imagery_format}' not in '{imagery_formats_str}'.")

    if ("category" in source["properties"]
            and "photo" in source["properties"]["category"]):
        if "jpeg" not in imagery_format and "jpeg" in imagery_formats_str:
            warning_msgs.append(
                f"Server supports JPEG, but '{imagery_format}' is used. "
                "JPEG is typically preferred for photo sources, but might not be always "
                "the best choice. "
                f"(Server supports: '{imagery_formats_str}')")
Exemple #3
0
async def update_wms(wms_url, session: ClientSession, messages):
    """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_args = {}
    u = urlparse(wms_url)
    for k, v in parse_qsl(u.query, keep_blank_values=True):
        wms_args[k.lower()] = v

    layers = wms_args["layers"].split(",")

    # Fetch highest supported WMS GetCapabilities
    wms = None
    for wmsversion in supported_wms_versions:
        # Do not try versions older than in the url
        if wmsversion < wms_args["version"]:
            continue

        try:
            wms_getcapabilites_url = wmshelper.get_getcapabilities_url(
                wms_url, wmsversion)
            messages.append(f"WMS url: {wms_getcapabilites_url}")
            resp = await get_url(wms_getcapabilites_url,
                                 session,
                                 with_text=True)
            if resp.exception is not None:
                messages.append(
                    f"Could not download GetCapabilites URL for {wms_getcapabilites_url}: {resp.exception}"
                )
                continue
            xml = resp.text
            if isinstance(xml, bytes):
                # Parse xml encoding to decode
                try:
                    xml_ignored = xml.decode(errors="ignore")
                    str_encoding = re.search('encoding="(.*?)"',
                                             xml_ignored).group(1)
                    xml = xml.decode(encoding=str_encoding)
                except Exception as e:
                    raise RuntimeError("Could not parse encoding: {}".format(
                        str(e)))

            wms = wmshelper.parse_wms(xml)
            if wms is not None:
                break
        except Exception as e:
            messages.append(
                f"Could not get GetCapabilites URL for {wmsversion}: {e}")

    if wms is None:
        # Could not request GetCapabilities
        messages.append("Could not contact WMS server")
        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["layers"]
    layers_advertised_lower_case = {
        layer.lower(): layer
        for layer in layers_advertised
    }
    updated_layers = []
    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_getcapabilites_url}"
            )
            return None
        else:
            updated_layers.append(layers_advertised_lower_case[layer_lower])
    layers = updated_layers

    wms_args["version"] = wms["version"]
    if wms_args["version"] == "1.3.0":
        wms_args.pop("srs", None)
        wms_args["crs"] = "{proj}"
    else:
        wms_args.pop("crs", None)
        wms_args["srs"] = "{proj}"
    if "styles" not in wms_args:
        wms_args["styles"] = ""

    def test_proj(proj):
        if proj == "CRS:84":
            return True
        if "AUTO" in proj:
            return False
        # 'EPSG:102067' is not valid, should be ESRI:102067: https://epsg.io/102067
        if proj == "EPSG:102067":
            return False
        # 'EPSG:102066' is not valid, should be ESRI:102066: https://epsg.io/102066
        if proj == "EPSG:102066":
            return False
        if "EPSG" in proj:
            try:
                CRS.from_string(proj)
                return True
            except:
                return False
        return False

    # For multilayer WMS queries only EPSG codes are valid that are available for all layers
    new_projections = wms["layers"][layers[0]]["CRS"]
    for layer in layers[1:]:
        new_projections.intersection_update(wms["layers"][layer]["CRS"])
    new_projections = set(
        [proj.upper() for proj in new_projections if test_proj(proj)])
    if len(new_projections) == 0:
        if len(layers) > 1:
            messages.append("No common valid projections among layers.")
        else:
            messages.append("No valid projections for layer.")
        return None

    new_wms_parameters = OrderedDict()
    for key in [
            "map",
            "layers",
            "styles",
            "format",
            "transparent",
            "crs",
            "srs",
            "width",
            "height",
            "bbox",
            "version",
            "service",
            "request",
    ]:
        if key in wms_args:
            new_wms_parameters[key] = wms_args[key]
    for key in wms_args:
        if key not in new_wms_parameters:
            new_wms_parameters[key] = wms_args[key]

    baseurl = wms_url.split("?")[0]
    url_parameters = "&".join([
        "{}={}".format(key.upper(), value)
        for key, value in new_wms_parameters.items()
    ])
    new_url = f"{baseurl}?{url_parameters}"

    return {"url": new_url, "available_projections": new_projections}