diff --git a/source/gui/settingsDialogs.py b/source/gui/settingsDialogs.py index d8df8d1461..71b31e39d4 100644 --- a/source/gui/settingsDialogs.py +++ b/source/gui/settingsDialogs.py @@ -999,6 +999,7 @@ def onChangeMirrorURL(self, evt: wx.CommandEvent | wx.KeyEvent): configPath=("update", "serverURL"), helpId="SetURLDialog", urlTransformer=lambda url: f"{url}?versionType=stable", + responseValidator=_isResponseUpdateMirrorValid, ) ret = changeMirror.ShowModal() if ret == wx.ID_OK: @@ -5595,3 +5596,18 @@ def _isResponseAddonStoreCacheHash(response: requests.Response) -> bool: # While the NV Access Add-on Store cache hash is a git commit hash as a string, other implementations may use a different format. # Therefore, we only check if the data is a non-empty string. return isinstance(data, str) and bool(data) + + +def _isResponseUpdateMirrorValid(response: requests.Response) -> bool: + if not response.ok: + return False + + responseContent = response.text + + try: + parsedResponse = updateCheck.parseUpdateCheckResponse(responseContent) + except Exception as e: + log.error(f"Error parsing update mirror response: {e}") + return False + + return parsedResponse is not None diff --git a/source/updateCheck.py b/source/updateCheck.py index 6207831f2f..8fa47146b9 100644 --- a/source/updateCheck.py +++ b/source/updateCheck.py @@ -118,6 +118,27 @@ def getQualifiedDriverClassNameForStats(cls): return "%s (core)" % name +def parseUpdateCheckResponse(data: str) -> dict[str, str] | None: + """ + Parses the update response and returns a dictionary with metadata. + + :param data: The raw server response as a UTF-8 decoded string. + :return: A dictionary containing the update metadata, or None if the format is invalid. + """ + if not data.strip(): + return None + + metadata = {} + for line in data.splitlines(): + try: + key, val = line.split(": ", 1) + metadata[key] = val + except ValueError: + return None # Invalid format + + return metadata + + UPDATE_FETCH_TIMEOUT_S = 30 # seconds @@ -187,15 +208,10 @@ def checkForUpdate(auto: bool = False) -> Optional[Dict]: raise if res.code != 200: raise RuntimeError("Checking for update failed with code %d" % res.code) - info = {} - for line in res: - # #9819: update description resource returns bytes, so make it Unicode. - line = line.decode("utf-8").rstrip() - try: - key, val = line.split(": ", 1) - except ValueError: - raise RuntimeError("Error in update check output") - info[key] = val + + data = res.text + info = parseUpdateCheckResponse(data) + if not info: return None return info diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 505f12d674..96e2827f1a 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -16,7 +16,7 @@ To use this feature, "allow NVDA to control the volume of other applications" mu * NVDA can now report when a link destination points to the current page. (#141, @LeonarddeR, @nvdaes) * Added an action in the Add-on Store to cancel the install of add-ons. (#15578, @hwf1324) * Added an action in the Add-on Store to retry the installation if the download/installation of an add-on fails. (#17090, @hwf1324) -* It is now possible to specify mirror URLs to use for NVDA updates and the Add-on Store. (#14974, #17151) +* It is now possible to specify mirror URLs to use for NVDA updates and the Add-on Store. (#14974, #17151, #17310) * The add-ons lists in the Add-on Store can be sorted by columns, including publication date, in ascending and descending order. (#15277, #16681, @nvdaes) * When decreasing or increasing the font size in LibreOffice Writer using the corresponding keyboard shortcuts, NVDA announces the new font size. (#6915, @michaelweghorn) * When applying the "Body Text" or a heading paragraph style using the corresponding keyboard shortcut in LibreOffice Writer 25.2 or newer, NVDA announces the new paragraph style. (#6915, @michaelweghorn)