Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to specify NVDA update check URL from within NVDA #17151

Merged
merged 38 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
dbfcfc9
Added update mirror setting (not yet used)
SaschaCowley Sep 10, 2024
11c4148
Changed update checker to use getter function, and made default check…
SaschaCowley Sep 10, 2024
ecaafa9
Changed name of config key
SaschaCowley Sep 10, 2024
2bf7b52
Added more helpful error messages
SaschaCowley Sep 10, 2024
b735ee5
Added a user guide section and context help binding to the settings c…
SaschaCowley Sep 10, 2024
ebc5255
Added changes entries
SaschaCowley Sep 10, 2024
711fdcd
Updated from % formatted strings to f-strings in a couple of places
SaschaCowley Sep 10, 2024
31b148a
Merge branch 'master' into updateMirror
SaschaCowley Sep 10, 2024
9566c4f
WIP settings dialog
SaschaCowley Sep 11, 2024
61ed2bb
PoC of a mirror dialog
SaschaCowley Sep 12, 2024
69a013b
UI improvements
SaschaCowley Sep 13, 2024
7d6d3b0
Added some type hints, slight reorganisation, and use transformer
SaschaCowley Sep 16, 2024
505a765
Refactored success and failure states into their own functions.
SaschaCowley Sep 16, 2024
244ea00
Added comments and translation strings
SaschaCowley Sep 16, 2024
9a4c49f
Documentation and translation improvements
SaschaCowley Sep 16, 2024
a0ea33b
Up[dated normalization logic
SaschaCowley Sep 16, 2024
59d68db
Added warning when trying to save untested or test failed URLs
SaschaCowley Sep 17, 2024
a62e99e
Disable update mirror when in secure mode
SaschaCowley Sep 17, 2024
78ad993
Made mirror url dialog title more descriptive
SaschaCowley Sep 17, 2024
6870ae2
Improved normalization and onOk logic.
SaschaCowley Sep 17, 2024
08ae1c8
Updated user guide and added context help bindings
SaschaCowley Sep 17, 2024
19290bf
Made missed string translatable
SaschaCowley Sep 17, 2024
f1f8bae
Switched to yes/no/cancel buttons
SaschaCowley Sep 18, 2024
14c2195
Reordered some clean-up to try and avoid intermitant lifecycle issues
SaschaCowley Sep 18, 2024
9fc0994
Merge branch 'master' into updateMirror
SaschaCowley Sep 18, 2024
737b219
Updated translator string to be more generic
SaschaCowley Sep 18, 2024
b2b72b8
Removed left over reference to removed cleanup method
SaschaCowley Sep 19, 2024
649cc00
Fix NVDA reporting the old mirror URL if focus is returned from the m…
SaschaCowley Sep 19, 2024
f6eb04d
Removed some old code
SaschaCowley Sep 20, 2024
64bf50a
Improved type hints
SaschaCowley Sep 20, 2024
638f86b
Improve comment
SaschaCowley Sep 20, 2024
22668a4
Made SetURLDialog private
SaschaCowley Sep 20, 2024
8c77692
Update user_docs/en/userGuide.md
SaschaCowley Sep 20, 2024
c7b6998
Changed text of mirror URL read only text field from '(None)' to 'No …
SaschaCowley Sep 22, 2024
613bfab
Removed colon after 'Update mirror'
SaschaCowley Sep 22, 2024
a092c11
Updated user guide with new settings wording
SaschaCowley Sep 22, 2024
a898547
Narrow type hint
SaschaCowley Sep 23, 2024
cf9c91f
Moved _SetURLDialog to its own module
SaschaCowley Sep 23, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions source/config/configSpec.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@
startupNotification = boolean(default=true)
allowUsageStats = boolean(default=false)
askedAllowUsageStats = boolean(default=false)
serverURL = string(default="")

[inputComposition]
autoReportAllCandidates = boolean(default=True)
Expand Down
217 changes: 217 additions & 0 deletions source/gui/_SetURLDialog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2024, NV Access Limited
# This file may be used under the terms of the GNU General Public License, version 2 or later.
# For more details see: https://www.gnu.org/licenses/gpl-2.0.html

import threading
from enum import Enum, auto
from typing import Callable, Iterable

import config
import requests
import wx
from logHandler import log
from requests.exceptions import RequestException
from url_normalize import url_normalize

import gui

from . import guiHelper
from .settingsDialogs import SettingsDialog


class _SetURLDialog(SettingsDialog):
class _URLTestStatus(Enum):
UNTESTED = auto()
PASSED = auto()
FAILED = auto()

_progressDialog: "gui.IndeterminateProgressDialog | None" = None
_testStatus: _URLTestStatus = _URLTestStatus.UNTESTED

def __init__(
self,
parent: wx.Window,
title: str,
configPath: Iterable[str],
helpId: str | None = None,
urlTransformer: Callable[[str], str] = lambda url: url,
*args,
**kwargs,
):
"""Customisable dialog for requesting a URL from the user.

:param parent: Parent window of this dialog.
:param title: Title of this dialog.
:param configPath: Where in the config the URL is to be stored.
:param helpId: Anchor of the user guide section for this dialog, defaults to None
:param urlTransformer: Function to transform the given URL into something usable, eg by adding required query parameters. Defaults to the identity function.
:raises ValueError: If no config path is given.
"""
if not configPath or len(configPath) < 1:
raise ValueError("Config path not provided.")
self.title = title
self.helpId = helpId
self._configPath = configPath
self._urlTransformer = urlTransformer
super().__init__(parent, *args, **kwargs)

def makeSettings(self, settingsSizer: wx.Sizer):
settingsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=settingsSizer)
self._urlControl = urlControl = settingsSizerHelper.addLabeledControl(
# Translators: The label of a text box asking the user for a URL.
# The purpose of this text box will be explained elsewhere in the user interface.
_("&URL:"),
wx.TextCtrl,
size=(250, -1),
)
self.bindHelpEvent("UpdateMirrorURL", urlControl)
self._testButton = testButton = wx.Button(
self,
# Translators: A button in a dialog which allows the user to test a URL that they have entered.
label=_("&Test..."),
)
self.bindHelpEvent("UpdateMirrorTest", testButton)
urlControlsSizerHelper = guiHelper.BoxSizerHelper(self, sizer=urlControl.GetContainingSizer())
urlControlsSizerHelper.addItem(testButton)
testButton.Bind(wx.EVT_BUTTON, self._onTest)
# We need to bind the text change handler before setting the text of the URL control so that it gets called when we populate the URL control.
# This allows us to rely on the URL control emitting the text event which will enable the test button, rather than having to doo so manually.
urlControl.Bind(wx.EVT_TEXT, self._onTextChange)
self._url = self._getFromConfig()

def postInit(self):
# Ensure that focus is on the URL text box.
self._urlControl.SetFocus()

def onOk(self, evt: wx.CommandEvent):
self._normalize()
if self._url == self._getFromConfig():
shouldSave = False
elif self._url and self._testStatus != _SetURLDialog._URLTestStatus.PASSED:
ret = gui.messageBox(
_(
# Translators: Message shown to users when saving a potentially invalid URL to NVDA's settings.
"The URL you have entered failed the connection test. Are you sure you want to save it anyway?",
)
if self._testStatus == _SetURLDialog._URLTestStatus.FAILED
else _(
# Translators: Message shown to users when saving an untested URL to NVDA's settings.
"The URL you have entered has not been tested. Are you sure you want to save it without attempting to connect to it first?",
),
# Translators: The title of a dialog.
_("Warning"),
wx.YES_NO | wx.CANCEL | wx.CANCEL_DEFAULT | wx.ICON_WARNING,
self,
)
if ret == wx.YES:
shouldSave = True
elif ret == wx.NO:
shouldSave = False
else:
return
else:
shouldSave = True

if shouldSave:
self._saveToConfig()
# Hack: Update the mirror URL in the parent window before closing.
# Otherwise, if focus is immediately returned to the mirror URL text control, NVDA will report the old value even though the new one is reflected visually.
self.Parent._updateCurrentMirrorURL()
super().onOk(evt)

def _onTextChange(self, evt: wx.CommandEvent):
"""Enable the "Test..." button only when there is text in the URL control, and change the URL's test status when the URL changes."""
value = self._url
self._testButton.Enable(not (len(value) == 0 or value.isspace()))
self._testStatus = _SetURLDialog._URLTestStatus.UNTESTED

def _onTest(self, evt: wx.CommandEvent):
"""Normalize the URL, start a background thread to test it, and show an indeterminate progress dialog to the user."""
self._normalize()
t = threading.Thread(
name=f"{self.__class__.__module__}.{self._onTest.__qualname__}",
target=self._bg,
daemon=True,
)
self._progressDialog = gui.IndeterminateProgressDialog(
self,
title=self.title,
# Translators: A message shown to users when connecting to a URL to ensure it is valid.
message=_("Validating URL..."),
)
t.start()

def _bg(self):
"""Background URL connection thread."""
try:
with requests.get(self._urlTransformer(self._url)) as r:
r.raise_for_status()
self._success()
except RequestException as e:
log.debug(f"Failed to check URL: {e}")
self._failure(e)

def _success(self):
"""Notify the user that we successfully connected to their URL."""
wx.CallAfter(self._progressDialog.done)
self._progressDialog = None
self._testStatus = _SetURLDialog._URLTestStatus.PASSED
wx.CallAfter(
gui.messageBox,
# Translators: Message shown to users when testing a given URL has succeeded.
_("Successfully connected to the given URL."),
# Translators: The title of a dialog presented when a test succeeds
_("Success"),
wx.OK,
)

def _failure(self, error: Exception):
"""Notify the user that testing their URL failed."""
wx.CallAfter(self._progressDialog.done)
self._progressDialog = None
self._testStatus = _SetURLDialog._URLTestStatus.FAILED
wx.CallAfter(
gui.messageBox,
_(
# Translators: Message displayed to users when testing a URL has failed.
"Unable to connect to the given URL. Check that you are connected to the internet and the URL is correct.",
),
# Translators: The title of a dialog presented when an error occurs.
"Error",
wx.OK | wx.ICON_ERROR,
)

def _normalize(self):
"""Normalize the URL in the URL text box."""
current_url = self._url
normalized_url = url_normalize(self._url.strip()).rstrip("/")
if current_url != normalized_url:
# Only change the value of the textbox if the value has actually changed, as EVT_TEXT will be fired even if the replacement text is identical.
self._url = normalized_url

def _getFromConfig(self) -> str:
"""Get the value pointed to by `configPath` from the config."""
currentConfigSection = config.conf
keyIndex = len(self._configPath) - 1
for index, component in enumerate(self._configPath):
if index == keyIndex:
return currentConfigSection[component]
currentConfigSection = currentConfigSection[component]

def _saveToConfig(self):
"""Save the value of `_url` to the config."""
currentConfigSection = config.conf
keyIndex = len(self._configPath) - 1
for index, component in enumerate(self._configPath):
if index == keyIndex:
currentConfigSection[component] = self._url
currentConfigSection = currentConfigSection[component]

@property
def _url(self) -> str:
return self._urlControl.GetValue()

@_url.setter
def _url(self, val: str):
self._urlControl.SetValue(val)
76 changes: 76 additions & 0 deletions source/gui/settingsDialogs.py
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,7 @@ def makeSettings(self, settingsSizer):
if globalVars.appArgs.secure or not config.isInstalledCopy():
self.copySettingsButton.Disable()
settingsSizerHelper.addItem(self.copySettingsButton)

if updateCheck:
item = self.autoCheckForUpdatesCheckBox = wx.CheckBox(
self,
Expand Down Expand Up @@ -953,6 +954,66 @@ def makeSettings(self, settingsSizer):
item.Disable()
settingsSizerHelper.addItem(item)

# Translators: The label for the update mirror on the General Settings panel.
mirrorBoxSizer = wx.StaticBoxSizer(wx.HORIZONTAL, self, label=_("Update mirror"))
mirrorBox = mirrorBoxSizer.GetStaticBox()
mirrorBoxSizerHelper = guiHelper.BoxSizerHelper(self, sizer=mirrorBoxSizer)
settingsSizerHelper.addItem(mirrorBoxSizerHelper)

# Use an ExpandoTextCtrl because even when read-only it accepts focus from keyboard, which
# standard read-only TextCtrl does not. ExpandoTextCtrl is a TE_MULTILINE control, however
# by default it renders as a single line. Standard TextCtrl with TE_MULTILINE has two lines,
# and a vertical scroll bar. This is not neccessary for the single line of text we wish to
# display here.
# Note: To avoid code duplication, the value of this text box will be set in `onPanelActivated`.
self.mirrorURLTextBox = ExpandoTextCtrl(
mirrorBox,
size=(self.scaleSize(250), -1),
style=wx.TE_READONLY,
)
# Translators: This is the label for the button used to change the NVDA update mirror URL,
# it appears in the context of the update mirror group on the General page of NVDA's settings.
changeMirrorBtn = wx.Button(mirrorBox, label=_("Change..."))
mirrorBoxSizerHelper.addItem(
guiHelper.associateElements(
self.mirrorURLTextBox,
changeMirrorBtn,
),
)
self.bindHelpEvent("UpdateMirror", mirrorBox)
self.mirrorURLTextBox.Bind(wx.EVT_CHAR_HOOK, self._enterTriggersOnChangeMirrorURL)
changeMirrorBtn.Bind(wx.EVT_BUTTON, self.onChangeMirrorURL)
if globalVars.appArgs.secure:
SaschaCowley marked this conversation as resolved.
Show resolved Hide resolved
mirrorBox.Disable()

def onChangeMirrorURL(self, evt: wx.CommandEvent | wx.KeyEvent):
"""Show the dialog to change the update mirror URL, and refresh the dialog in response to the URL being changed."""
# Import late to avoid circular dependency.
from gui._SetURLDialog import _SetURLDialog

changeMirror = _SetURLDialog(
self,
# Translators: Title of the dialog used to change NVDA's update server mirror URL.
title=_("Set NVDA Update Mirror"),
configPath=("update", "serverURL"),
helpId="SetUpdateMirror",
urlTransformer=lambda url: f"{url}?versionType=stable",
)
ret = changeMirror.ShowModal()
if ret == wx.ID_OK:
self.Freeze()
# trigger a refresh of the settings
self.onPanelActivated()
self._sendLayoutUpdatedEvent()
self.Thaw()

def _enterTriggersOnChangeMirrorURL(self, evt: wx.KeyEvent):
"""Open the change update mirror URL dialog in response to the enter key in the mirror URL read-only text box."""
if evt.KeyCode == wx.WXK_RETURN:
self.onChangeMirrorURL(evt)
else:
evt.Skip()

def onCopySettings(self, evt):
if os.path.isdir(WritePaths.addonsDir) and 0 < len(os.listdir(WritePaths.addonsDir)):
message = _(
Expand Down Expand Up @@ -1046,6 +1107,21 @@ def onSave(self):
updateCheck.terminate()
updateCheck.initialize()

def onPanelActivated(self):
if updateCheck:
self._updateCurrentMirrorURL()
super().onPanelActivated()

def _updateCurrentMirrorURL(self):
self.mirrorURLTextBox.SetValue(
(
url
if (url := config.conf["update"]["serverURL"])
# Translators: A value that appears in NVDA's Settings to indicate that no mirror is in use.
else _("No mirror")
),
)

def postSave(self):
if self.oldLanguage != config.conf["general"]["language"]:
LanguageRestartDialog(self).ShowModal()
Expand Down
35 changes: 30 additions & 5 deletions source/updateCheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
from utils.tempFile import _createEmptyTempFileForDeletingFile

#: The URL to use for update checks.
CHECK_URL = "https://www.nvaccess.org/nvdaUpdateCheck"
_DEFAULT_CHECK_URL = "https://www.nvaccess.org/nvdaUpdateCheck"
#: The time to wait between checks.
CHECK_INTERVAL = 86400 # 1 day
#: The time to wait before retrying a failed check.
Expand All @@ -91,6 +91,12 @@
autoChecker: Optional["AutoUpdateChecker"] = None


def _getCheckURL() -> str:
if url := config.conf["update"]["serverURL"]:
return url
return _DEFAULT_CHECK_URL


def getQualifiedDriverClassNameForStats(cls):
"""fetches the name from a given synthDriver or brailleDisplay class, and appends core for in-built code, the add-on name for code from an add-on, or external for code in the NVDA user profile.
Some examples:
Expand Down Expand Up @@ -162,7 +168,7 @@ def checkForUpdate(auto: bool = False) -> Optional[Dict]:
"outputBrailleTable": config.conf["braille"]["translationTable"] if brailleDisplayClass else None,
}
params.update(extraParams)
url = "%s?%s" % (CHECK_URL, urllib.parse.urlencode(params))
url = f"{_getCheckURL()}?{urllib.parse.urlencode(params)}"
try:
log.debug(f"Fetching update data from {url}")
res = urllib.request.urlopen(url, timeout=UPDATE_FETCH_TIMEOUT_S)
Expand Down Expand Up @@ -331,12 +337,31 @@ def _started(self):
)

def _error(self):
if url := config.conf["update"]["serverURL"]:
tip = pgettext(
"updateCheck",
# Translators: A suggestion of what to do when checking for NVDA updates fails and an update mirror is being used.
# {url} will be replaced with the mirror URL.
"Make sure you are connected to the internet, and the NVDA update mirror URL is valid.\n"
"Mirror URL: {url}",
).format(url=url)
else:
tip = pgettext(
"updateCheck",
# Translators: A suggestion of what to do when fetching add-on data from the store fails and the default metadata URL is being used.
"Make sure you are connected to the internet and try again.",
)
message = pgettext(
"updateCheck",
# Translators: A message indicating that an error occurred while checking for an update to NVDA.
# tip will be replaced with a context sensitive suggestion of next steps.
"Error checking for update.\n{tip}",
).format(tip=tip)
wx.CallAfter(self._progressDialog.done)
self._progressDialog = None
wx.CallAfter(
gui.messageBox,
# Translators: A message indicating that an error occurred while checking for an update to NVDA.
_("Error checking for update."),
message,
# Translators: The title of an error message dialog.
_("Error"),
wx.OK | wx.ICON_ERROR,
Expand Down Expand Up @@ -943,7 +968,7 @@ def _updateWindowsRootCertificates():
with requests.get(
# We must specify versionType so the server doesn't return a 404 error and
# thus cause an exception.
CHECK_URL + "?versionType=stable",
f"{_getCheckURL()}?versionType=stable",
timeout=UPDATE_FETCH_TIMEOUT_S,
# Use an unverified connection to avoid a certificate error.
verify=False,
Expand Down
Loading