def serializeServers() -> Union[str, None]: """ Calls `getSettings()` on all initialized serializable servers and returns them as a JSON string. """ global _instances server_config = [] bad_servers = [] # Test servers for s in _instances.values(): server_tuple = _getServerAsTuple(s) if not server_tuple: feedback.logError(f"Server configuration for '{s.serverName}' cannot be saved and will be removed.'") bad_servers.append(s.serverName) continue server_config.append(server_tuple) # Remove bad servers (if any) while bad_servers: del _instances[bad_servers.pop()] # Serialize JSON and output string try: config_str = json.dumps(server_config) except TypeError as e: feedback.logError(f"Failed to serialize server configuration as JSON: {e}") return return config_str
def getToken(self): """ Requests a session token using POST and reads its value from the cookie. """ headers = {'Content-Type': 'application/xml'} feedback.logInfo(f'POST {self._token_url}') # Note: it is expected that this returns a 403 self.post(self._token_url, headers=headers) token = self.cookies.get(GeonetworkSession.COOKIE_TOKEN) if not token: feedback.logError( f'Did not receive a {GeonetworkSession.COOKIE_TOKEN} cookie!') return token
def parseMe(response: requests.Response) -> bool: """ Parses the response from a 'me' request to a GeoNetwork API. Returns True if the response was authenticated. """ try: xml = ETree.fromstring(response.text) authenticated = xml.find('me').get('authenticated').lower() except (TypeError, AttributeError, ETree.ParseError): feedback.logError( f'Received invalid response from {response.url}: {response.text}') return False return authenticated == 'true'
def signedIn(self) -> bool: """ Returns True if the session is authenticated (i.e. the user has been signed in). """ headers = {'Accept': 'application/xml'} feedback.logInfo(f'GET {self._token_url}') result = self.get(self._token_url, headers=headers) if result.status_code < 400: return parseMe(result) # This should not happen (even unauthenticated responses should return a 200), but we have to handle it feedback.logError( f'Failed to query {self._token_url}: server returned {result.status_code}' ) return False
def run(self): results = [] for count, item in enumerate(self._items, 1): try: results.append(self._func(item)) except Exception as e: # Log a QGIS error message (should be thread-safe) logError(e) self.progress.emit(count) if self._stop: break self.finished.emit(results)
def loadAlgorithms(self): for server_type in getServerTypes(): algorithm = server_type.getAlgorithmInstance() if not algorithm: # Server type does not provide processing algorithm continue if not isinstance(algorithm, BridgeAlgorithm): feedback.logError( f"Skipped algorithm returned by {server_type.__name__}: " f"instance does not inherit {BridgeAlgorithm.__name__}") continue self.addAlgorithm(algorithm)
def load(self): try: ProcessingConfig.settingIcons[self.name()] = self.icon() ProcessingConfig.addSetting( Setting(self.name(), self.BRIDGE_ACTIVE, self.tr('Activate'), False)) ProcessingConfig.readSettings() self.refreshAlgorithms() except Exception as err: feedback.logError(err) return False return True
def saveServer(server, replace_key: str) -> bool: """ Adds the given server (instance) to the Bridge server settings. Returns True if the instance was successfully saved. :param server: The new server instance to store. :param replace_key: The key (server name) under which to save the server instance. If the key matches the `server.serverName`, the server is updated under that key. If it doesn't match, the server instance under the given key is deleted and a new instance is saved under a new key equal to the `server.serverName`. :returns: True if save was successful. :raises: ValueError if `replace_key` does not match the `server.serverName` and the new `server.serverName` already exists in the `_instances` dictionary (duplicate key), or if `server.serverName` is empty. """ global _instances if not isinstance(server, (bases.ServerBase, bases.CombiServerBase)): # This should not happen if the server properly implements (Combi)ServerBase feedback.logError(f"Cannot store server of type '{server.__class__.__name__}': " f"must implement {bases.ServerBase.__name__} or {bases.CombiServerBase.__name__}") return False if not server.serverName: # Make sure that the server has a non-empty name raise ValueError('server name cannot be empty') if replace_key != server.serverName: # User has renamed the server: check if new name does not exist yet if server.serverName in getServerNames(): raise ValueError(f"server named '{server.serverName}' already exists") try: # Remove instance under old name del _instances[replace_key] except KeyError: feedback.logWarning(f"server named '{replace_key}' does not exist") # Save instance using (new) server name as key _instances[server.serverName] = server if isinstance(server, bases.CatalogServerBase): server.addOGCServices() if not saveConfiguredServers(): # Remove server again if the instance could not be saved in QGIS settings del _instances[server.serverName] return False return True
def _getServerAsTuple(server): """ Verifies that the given server instance can be recreated using the values from `getSettings()`. If this is the case, a tuple of (type, params) will be returned. Otherwise, None is returned. """ try: params = server.getSettings() or {} except (AttributeError, NotImplementedError): # This should not happen if the server properly implements AbstractServer feedback.logError(f"Server does not implement {bases.AbstractServer.__name__}.getSettings") return server_type = server.__class__.__name__ try: # Test that the server class can be instantiated again using the output from getSettings() _initServer(server_type, **params) except ServerInitError as e: # Report error that the server cannot be initialized with the given parameters feedback.logError(f"{server_type}.getSettings() returned bad parameters: {e}") return return server_type, params
def signIn(self, user: str, pwd: str, refresh_token: bool = False) -> bool: """ Checks if the current session is authenticated. If not, a token is retrieved (if not available yet) and its value is set in the session X-XSRF-TOKEN header. Finally, the user will be signed in to GeoNetwork. :param user: The basic authentication user name. :param pwd: The basic authentication password. :param refresh_token: If True (default = False), the token will be refreshed. :returns: True if sign in was successful. """ if GeonetworkSession.HEADER_TOKEN in self.headers and self.signedIn and not refresh_token: # We are still signed in: no need to do it again return True if not (user and pwd): return False auth = {'username': user, 'password': pwd} if GeonetworkSession.HEADER_TOKEN not in self.headers or refresh_token: # Obtain cookie token if there is none or we want to refresh it token = self.getToken() if token: # Update session headers with an X-XSRF-TOKEN self.headers[GeonetworkSession.HEADER_TOKEN] = token auth['_csrf'] = self.headers[GeonetworkSession.HEADER_TOKEN] headers = {'Accept': 'application/html'} feedback.logInfo(f'POST {self._signin_url}') # Note: it is **extremely** important to NOT allow redirects here! # Disallowing redirects will sign us in successfully and return a 302 (Found). # A redirect however will result in a 403 (Access Denied), even with valid credentials. result = self.post(self._signin_url, data=auth, headers=headers, allow_redirects=False) status = result.status_code if status >= 400: prefix = f"Failed to sign in to {self._signin_url}" if status == 403: if not refresh_token: # Retry 1 more time with a refreshed token feedback.logWarning(f"{prefix}: retrying with new token") return self.signIn(user, pwd, True) feedback.logError( f"{prefix}: access denied to user '{user}' ({status})") elif status == 401: feedback.logError( f"{prefix}: user '{user}' not authorized (bad credentials)" ) else: feedback.logError( f"{prefix}: server returned {status} (please check server logs)" ) return False return True
def deserializeServers(config_str: str) -> bool: """ Deserializes a JSON server configuration string and creates Bridge server instances. :param config_str: A JSON string. Must be deserializable as a list of (str, dict). :returns: True when successful, False otherwise. """ global _instances # Parse JSON object from settings string try: stored_servers = json.loads(config_str) except json.JSONDecodeError as e: feedback.logError(f"Failed to parse servers configuration: {e}") return False # It is expected that `stored_servers` is a list if not isinstance(stored_servers, list) or len(stored_servers) == 0: feedback.logError("Server configuration must be a non-empty list") return False # Instantiate servers from models and settings num_added = 0 for (server_type, properties) in stored_servers: name = properties.get('name') if not name: feedback.logWarning(f'Skipped {server_type} entry due to missing name') continue key = getUniqueName(name) if key != name: properties['name'] = key try: s = _initServer(server_type, **properties) except (UnknownServerError, ServerInitError) as e: feedback.logError(f"Failed to load {server_type} type: {e}") continue if key != name: feedback.logWarning(f"Changed name from '{name}' to '{key}' for " f"non-unique {s.getLabel()} entry") _instances[key] = s num_added += 1 # Return True if any of the servers initialized successfully return num_added > 0
def run(self): def publishLayer(lyr, lyr_name): fields = None if lyr.type() == lyr.VectorLayer: fields = [ _name for _name, publish in self.field_map[lyr.id()].items() if publish ] self.geodata_server.publishLayer(lyr, fields) if self.metadata_server is not None: metadata_uuid = uuidForLayer(lyr) md_url = self.metadata_server.metadataUrl(metadata_uuid) self.geodata_server.setLayerMetadataLink(lyr_name, md_url) try: validator = QgsNativeMetadataValidator() # FIXME: remove or improve this # DONOTALLOW = 0 ALLOW = 1 ALLOWONLYDATA = 2 allow_without_md = ALLOW # pluginSetting("allowWithoutMetadata") if self.geodata_server is not None: self.geodata_server.prepareForPublishing(self.only_symbology) self.results = {} for i, layer_id in enumerate(self.layer_ids): if self.isCanceled(): return False warnings, errors = [], [] self.setProgress(i * 100 / len(self.layer_ids)) layer = lyr_utils.getLayerById(layer_id) name, safe_name = lyr_utils.getLayerTitleAndName(layer) if not lyr_utils.hasValidLayerName(layer): try: warnings.append( f"Layer name '{name}' contains characters that may cause issues" ) except UnicodeError: warnings.append( "Layer name contains characters that may cause issues" ) md_valid, _ = validator.validate(layer.metadata()) if self.geodata_server is not None: self.geodata_server.resetLogIssues() # Publish style self.stepStarted.emit(layer_id, SYMBOLOGY) try: self.geodata_server.publishStyle(layer) except: errors.append(traceback.format_exc()) self.stepFinished.emit(layer_id, SYMBOLOGY) if self.only_symbology: # Skip data publish if "only symbology" was checked self.stepSkipped.emit(layer_id, DATA) else: # Publish data self.stepStarted.emit(layer_id, DATA) try: if md_valid or allow_without_md in (ALLOW, ALLOWONLYDATA): publishLayer(layer, safe_name) else: self.stepStarted.emit(layer_id, DATA) if md_valid or allow_without_md in ( ALLOW, ALLOWONLYDATA): publishLayer(layer, safe_name) else: self.geodata_server.logError( f"Layer '{name}' has invalid metadata. " f"Layer was not published") self.stepFinished.emit(layer_id, DATA) except: errors.append(traceback.format_exc()) self.stepFinished.emit(layer_id, DATA) else: # No geodata server selected: skip layer data and symbology self.stepSkipped.emit(layer_id, SYMBOLOGY) self.stepSkipped.emit(layer_id, DATA) if self.metadata_server is not None: # User selected metadata server: publish metadata try: self.metadata_server.resetLogIssues() if md_valid or allow_without_md == ALLOW: wms = None wfs = None full_name = None if self.geodata_server is not None: full_name = self.geodata_server.fullLayerName( safe_name) wms = self.geodata_server.getWmsUrl() if layer.type() == layer.VectorLayer: wfs = self.geodata_server.getWfsUrl() self.autofillMetadata(layer) self.stepStarted.emit(layer_id, METADATA) self.metadata_server.publishLayerMetadata( layer, wms, wfs, full_name) self.stepFinished.emit(layer_id, METADATA) else: self.metadata_server.logError( f"Layer '{name}' has invalid metadata. " f"Metadata was not published") except: errors.append(traceback.format_exc()) else: self.stepSkipped.emit(layer_id, METADATA) # Collect all layer-specific errors and warnings (if any) if self.geodata_server is not None: w, e = self.geodata_server.getLogIssues() warnings.extend(w) errors.extend(e) if self.metadata_server is not None: w, e = self.metadata_server.getLogIssues() warnings.extend(w) errors.extend(e) self.results[name] = (set(warnings), set(errors)) # Create layer groups (if any) if self.geodata_server is not None: self.stepStarted.emit(None, GROUPS) try: # FIXME (did this ever work?) self.geodata_server.createGroups(self._layerGroups(), self.layer_ids) except Exception as err: # TODO: figure out where to properly put a warning or error message for this feedback.logError(f"Could not create layer groups: {err}") finally: try: # Call closePublishing(): for GeoServer, this will set up vector tiles, if enabled self.geodata_server.closePublishing() except Exception as err: feedback.logError( f"Failed to finalize publish task: {err}") self.stepFinished.emit(None, GROUPS) else: self.stepSkipped.emit(None, GROUPS) return True except Exception: self.exc_type, _, _ = sys.exc_info() self.exception = traceback.format_exc() return False