Skip to content

Commit f8b8e1c

Browse files
authored
beta to master (#20141)
2 parents e603629 + 3af3ef2 commit f8b8e1c

15 files changed

Lines changed: 6539 additions & 5429 deletions

File tree

source/NVDAObjects/IAccessible/ia2Web.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,6 @@ def _rolesGenerator(self) -> Generator[Optional[controlTypes.Role], None, None]:
104104

105105
class Ia2Web(IAccessible):
106106
IAccessibleTableUsesTableCellIndexAttrib = True
107-
# The IAccessibleText implementation in web browsers exposes embedded object
108-
# characters which need to be traversed to read the content. That isn't useful
109-
# to users.
110-
_shouldUseTextInfoForReading = False
111107

112108
def isDescendantOf(self, obj: "NVDAObjects.NVDAObject") -> bool:
113109
if obj.windowHandle != self.windowHandle:
@@ -288,9 +284,6 @@ class Figure(Ia2Web):
288284

289285
class Editor(Ia2Web, DocumentWithTableNavigation):
290286
TextInfo = MozillaCompoundTextInfo
291-
# MozillaCompoundTextInfo traverses embedded objects and is suitable for
292-
# presentation to users.
293-
_shouldUseTextInfoForReading = True
294287

295288
def _getTableCellAt(self, tableID, startPos, destRow, destCol):
296289
# Locate the table in the object ancestry of the given document position.

source/NVDAObjects/__init__.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1605,12 +1605,6 @@ def _get__hasNavigableText(self):
16051605
else:
16061606
return False
16071607

1608-
#: Whether the TextInfo should be used for the review cursor, the read current
1609-
#: line command, etc. This should be False where the TextInfo is only used
1610-
#: internally and doesn't provide text that is suitable for presentation to the
1611-
#: user; e.g. it includes raw embedded object characters.
1612-
_shouldUseTextInfoForReading: bool = True
1613-
16141608
def _get_hasIrrelevantLocation(self):
16151609
"""Returns whether the location of this object is irrelevant for mouse or magnification tracking or highlighting,
16161610
either because it is programatically hidden (State.INVISIBLE), off screen or the object has no location."""

source/_magnifier/commands.py

Lines changed: 22 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,46 +34,50 @@
3434
)
3535
from logHandler import log
3636

37-
PAN_ACTION_TO_EDGE_NAME = {
37+
PAN_ACTION_TO_EDGE_MESSAGES = {
3838
MagnifierAction.PAN_LEFT: pgettext(
3939
"magnifier",
40-
# Translators: Short name for the left edge, used in messages.
41-
"left",
40+
# Translators: Message announced when already at left edge of the screen while panning the magnified view
41+
"left edge",
4242
),
4343
MagnifierAction.PAN_RIGHT: pgettext(
4444
"magnifier",
45-
# Translators: Short name for the right edge, used in messages.
46-
"right",
45+
# Translators: Message announced when already at right edge of the screen while panning the magnified view
46+
"right edge",
4747
),
4848
MagnifierAction.PAN_UP: pgettext(
4949
"magnifier",
50-
# Translators: Short name for the top edge, used in messages.
51-
"top",
50+
# Translators: Message announced when already at top edge of the screen while panning the magnified view
51+
"top edge",
5252
),
5353
MagnifierAction.PAN_DOWN: pgettext(
5454
"magnifier",
55-
# Translators: Short name for the bottom edge, used in messages.
56-
"bottom",
55+
# Translators: Message announced when already at bottom edge of the screen while panning the magnified view
56+
"bottom edge",
5757
),
5858
MagnifierAction.PAN_LEFT_EDGE: pgettext(
5959
"magnifier",
60-
# Translators: Short name for the left edge, used in messages.
61-
"left",
60+
# Translators: Message announced when already at left edge of the screen while panning the magnified view
61+
# to left edge
62+
"left edge",
6263
),
6364
MagnifierAction.PAN_RIGHT_EDGE: pgettext(
6465
"magnifier",
65-
# Translators: Short name for the right edge, used in messages.
66-
"right",
66+
# Translators: Message announced when already at right edge of the screen while panning the magnified view
67+
# to right edge
68+
"right edge",
6769
),
6870
MagnifierAction.PAN_TOP_EDGE: pgettext(
6971
"magnifier",
70-
# Translators: Short name for the top edge, used in messages.
71-
"top",
72+
# Translators: Message announced when already at top edge of the screen while panning the magnified view
73+
# to top edge
74+
"top edge",
7275
),
7376
MagnifierAction.PAN_BOTTOM_EDGE: pgettext(
7477
"magnifier",
75-
# Translators: Short name for the bottom edge, used in messages.
76-
"bottom",
78+
# Translators: Message announced when already at left edge of the screen while panning the magnified view
79+
# to left edge
80+
"bottom edge",
7781
),
7882
}
7983

@@ -165,14 +169,7 @@ def pan(action: MagnifierAction) -> None:
165169
if magnifierIsActiveVerify(magnifier, action):
166170
hasMoved = magnifier._pan(action)
167171
if not hasMoved:
168-
edgeName = PAN_ACTION_TO_EDGE_NAME.get(action)
169-
ui.message(
170-
pgettext(
171-
"magnifier",
172-
# Translators: Message announced when arriving at the {edge} edge.
173-
"{edge} edge",
174-
).format(edge=edgeName),
175-
)
172+
ui.message(PAN_ACTION_TO_EDGE_MESSAGES[action])
176173

177174

178175
def toggleFilter() -> None:

source/_remoteClient/client.py

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,6 @@ def disconnectAsLeader(self):
295295

296296
def disconnectAsFollower(self):
297297
"""Close follower session and clean up related resources."""
298-
if self.followerTransport:
299-
self.followerTransport.transportConnectionFailed.unregister(self.onConnectAsFollowerFailed)
300298
self.followerSession.close()
301299
self.followerSession = None
302300
self.followerTransport = None
@@ -308,7 +306,7 @@ def disconnectAsFollower(self):
308306

309307
@alwaysCallAfter
310308
def onConnectAsLeaderFailed(self):
311-
if self.leaderTransport.successfulConnects == 0:
309+
if self.leaderTransport is not None and self.leaderTransport.successfulConnects == 0:
312310
log.error(f"Failed to connect to {self.leaderTransport.address}")
313311
self.disconnectAsLeader()
314312
# Translators: Title of the connection error dialog.
@@ -321,21 +319,6 @@ def onConnectAsLeaderFailed(self):
321319
style=wx.OK | wx.ICON_WARNING,
322320
)
323321

324-
@alwaysCallAfter
325-
def onConnectAsFollowerFailed(self):
326-
if self.followerTransport and self.followerTransport.successfulConnects == 0:
327-
log.error(f"Failed to connect to {self.followerTransport.address}")
328-
self.disconnectAsFollower()
329-
# Translators: Title of the connection error dialog.
330-
gui.messageBox(
331-
parent=gui.mainFrame,
332-
# Translators: Title of the connection error dialog.
333-
caption=pgettext("remote", "Error Connecting"),
334-
# Translators: Message shown when unable to connect to the remote computer.
335-
message=pgettext("remote", "Unable to connect to the remote computer"),
336-
style=wx.OK | wx.ICON_WARNING,
337-
)
338-
339322
def doConnect(self, evt: inputCore.InputGesture = None):
340323
"""Show connection dialog and handle connection initiation.
341324
@@ -451,7 +434,6 @@ def connectAsFollower(self, connectionInfo: ConnectionInfo):
451434
self.onFollowerCertificateFailed,
452435
)
453436
transport.transportConnected.register(self.onConnectedAsFollower)
454-
transport.transportConnectionFailed.register(self.onConnectAsFollowerFailed)
455437
transport.transportDisconnected.register(self.onDisconnectedAsFollower)
456438
transport.reconnectorThread.start()
457439
if self.menu:
@@ -478,15 +460,16 @@ def handleCertificateFailure(self, transport: RelayTransport):
478460
log.warning(f"Certificate validation failed for {transport.address}")
479461
self.lastFailAddress = transport.address
480462
self.lastFailKey = transport.channel
463+
self._lastFailFingerprint = transport.lastFailFingerprint
481464
self.disconnect(_silent=True)
482465
try:
483-
certHash = transport.lastFailFingerprint
484-
485-
wnd = dialogs.CertificateUnauthorizedDialog(None, fingerprint=certHash)
466+
wnd = dialogs.CertificateUnauthorizedDialog(None, fingerprint=self._lastFailFingerprint)
486467
a = wnd.ShowModal()
487468
if a == wx.ID_YES:
488469
config = configuration.getRemoteConfig()
489-
config["trustedCertificates"][hostPortToAddress(self.lastFailAddress)] = certHash
470+
config["trustedCertificates"][hostPortToAddress(self.lastFailAddress)] = (
471+
self._lastFailFingerprint
472+
)
490473
if a == wx.ID_YES or a == wx.ID_NO:
491474
return True
492475
except Exception as ex:
@@ -502,6 +485,7 @@ def onLeaderCertificateFailed(self):
502485
port=self.lastFailAddress[1],
503486
key=self.lastFailKey,
504487
insecure=True,
488+
trustedFingerprint=self._lastFailFingerprint,
505489
)
506490
self.connectAsLeader(connectionInfo=connectionInfo)
507491

@@ -514,6 +498,7 @@ def onFollowerCertificateFailed(self):
514498
port=self.lastFailAddress[1],
515499
key=self.lastFailKey,
516500
insecure=True,
501+
trustedFingerprint=self._lastFailFingerprint,
517502
)
518503
self.connectAsFollower(connectionInfo=connectionInfo)
519504

source/_remoteClient/connectionInfo.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,9 @@ class ConnectionInfo:
7171
insecure: bool = False
7272
"""Allow insecure connections without SSL/TLS, defaults to False"""
7373

74+
trustedFingerprint: str | None = None
75+
"""Specifies a single certificate fingerprint to trust if :attr:`insecure` is ``True``."""
76+
7477
def __post_init__(self) -> None:
7578
self.port = self.port or SERVER_PORT
7679
self.mode = ConnectionMode(self.mode)

source/_remoteClient/transport.py

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
from dataclasses import dataclass
3636
from logHandler import log
3737
from queue import Queue
38-
from typing import Any, Literal, Optional, Self
38+
from typing import Any, Literal, Optional, Self, cast
3939

4040
import wx
4141
from extensionPoints import Action, HandlerRegistrar
@@ -263,13 +263,16 @@ def __init__(
263263
address: tuple[str, int],
264264
timeout: int = 0,
265265
insecure: bool = False,
266+
*,
267+
trustedFingerprint: str | None = None,
266268
) -> None:
267269
"""Initialize the TCP transport.
268270
269271
:param serializer: Message serializer instance
270272
:param address: Remote address to connect to, as (host, port) tuple
271273
:param timeout: Connection timeout in seconds, defaults to 0
272274
:param insecure: Skip certificate verification, defaults to False
275+
:param trustedFingerprint: Certificate fingerprint to trust this session if connecting insecurely.
273276
"""
274277
super().__init__(serializer=serializer)
275278
self.closed: bool = False
@@ -304,6 +307,8 @@ def __init__(
304307
self.insecure: bool = insecure
305308
"""Whether to skip certificate verification"""
306309

310+
self._trustedFingerprint = trustedFingerprint
311+
307312
def run(self) -> None:
308313
"""
309314
Establishes a connection to the server and manages the transport lifecycle.
@@ -319,10 +324,9 @@ def run(self) -> None:
319324
thread, and enters the read loop. Upon disconnection, it clears the connected
320325
event, notifies about the transport disconnection, and performs cleanup.
321326
322-
Raises:
323-
ssl.SSLCertVerificationError: If SSL certificate verification fails and
324-
the fingerprint is not trusted.
325-
Exception: For any other exceptions during the connection process.
327+
:raises ssl.SSLCertVerificationError: If SSL certificate verification fails and
328+
the fingerprint is not trusted.
329+
:raises Exception: For any other exceptions during the connection process.
326330
"""
327331
self.closed = False
328332
try:
@@ -338,6 +342,7 @@ def run(self) -> None:
338342
except Exception:
339343
pass
340344
if self.isFingerprintTrusted(fingerprint):
345+
self._trustedFingerprint = fingerprint
341346
self.insecure = True
342347
return self.run()
343348
self.lastFailFingerprint = fingerprint
@@ -346,6 +351,20 @@ def run(self) -> None:
346351
except Exception:
347352
self.transportConnectionFailed.notify()
348353
raise
354+
# If connecting without certificate verification and we were given a fingerprint to trust,
355+
# check that the server's certificate matches it.
356+
if (
357+
self.insecure
358+
and self._trustedFingerprint is not None
359+
# Since this is a client-side socket, if connection was successful it will always return a certificate.
360+
and (fingerprint := self._derCert2fingerprint(cast(bytes, self.serverSock.getpeercert(True))))
361+
!= self._trustedFingerprint
362+
):
363+
self._disconnect()
364+
self.lastFailFingerprint = fingerprint
365+
self.transportCertificateAuthenticationFailed.notify()
366+
self.transportConnectionFailed.notify()
367+
return
349368
self.onTransportConnected()
350369
self.startQueueThread()
351370
self._readLoop()
@@ -366,13 +385,18 @@ def isFingerprintTrusted(self, fingerprint: str) -> bool:
366385
and config["trustedCertificates"][hostPortToAddress(self.address)] == fingerprint
367386
)
368387

388+
@staticmethod
389+
def _derCert2fingerprint(cert: bytes) -> str:
390+
"""Convert a DER-encoded certificate to a certificate fingerprint."""
391+
return hashlib.sha256(cert).hexdigest().lower()
392+
369393
def getHostFingerprint(self) -> str:
370394
tempConnection = self.createOutboundSocket(*self.address, insecure=True)
371395
tempConnection.connect(self.address)
372396
certBin = tempConnection.getpeercert(True)
373397
tempConnection.close()
374-
fingerprint = hashlib.sha256(certBin).hexdigest().lower()
375-
return fingerprint
398+
# Since this is a client-side socket, if connection was successful it will always return a certificate.
399+
return self._derCert2fingerprint(cast(bytes, certBin))
376400

377401
def startQueueThread(self) -> None:
378402
"""Start the outbound message queue thread."""
@@ -609,6 +633,8 @@ def __init__(
609633
connectionType: str | None = None,
610634
protocolVersion: int = PROTOCOL_VERSION,
611635
insecure: bool = False,
636+
*,
637+
trustedFingerprint: str | None = None,
612638
) -> None:
613639
"""Initialize a new RelayTransport instance.
614640
@@ -619,12 +645,14 @@ def __init__(
619645
:param connectionType: Connection type identifier, defaults to ``None``
620646
:param protocolVersion: Protocol version to use, defaults to :const:`PROTOCOL_VERSION`
621647
:param insecure: Whether to skip certificate verification, defaults to ``False``
648+
:param trustedFingerprint: Certificate fingerprint to trust this session if connecting insecurely.
622649
"""
623650
super().__init__(
624651
address=address,
625652
serializer=serializer,
626653
timeout=timeout,
627654
insecure=insecure,
655+
trustedFingerprint=trustedFingerprint,
628656
)
629657
log.info(f"Connecting to {address} channel {channel}")
630658
self.channel: str | None = channel
@@ -652,6 +680,7 @@ def create(cls, connectionInfo: ConnectionInfo, serializer: Serializer) -> Self:
652680
channel=connectionInfo.key,
653681
connectionType=connectionInfo.mode,
654682
insecure=connectionInfo.insecure,
683+
trustedFingerprint=connectionInfo.trustedFingerprint,
655684
)
656685

657686
def onConnected(self) -> None:

source/config/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ def _setSystemConfig(
373373
else:
374374
relativePath = os.path.relpath(curSourceDir, fromPath)
375375
curDestDir = os.path.join(toPath, relativePath)
376-
if not isMigration and relativePath == "addons":
376+
if not isMigration and relativePath.casefold() == "addons":
377377
_prepareToCopyAddons(fromPath, toPath, subDirs, addonsToCopy)
378378
if not os.path.isdir(curDestDir):
379379
os.makedirs(curDestDir)

source/core.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -950,9 +950,14 @@ def main():
950950
warnForNonEmptyDirectory=warnForNonEmptyDirectory,
951951
)
952952
elif not globalVars.appArgs.minimal:
953-
try:
953+
if screenCurtain.screenCurtain.enabled:
954+
# Translators: This is shown on a braille display (if one is connected) when NVDA starts with the screen curtain enabled.
955+
initialMessage = _("NVDA started with screen curtain enabled")
956+
else:
954957
# Translators: This is shown on a braille display (if one is connected) when NVDA starts.
955-
braille.handler.message(_("NVDA started"))
958+
initialMessage = _("NVDA started")
959+
try:
960+
braille.handler.message(initialMessage)
956961
except: # noqa: E722
957962
log.error("", exc_info=True)
958963
if globalVars.appArgs.launcher:

source/globalCommands.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -262,22 +262,15 @@ def script_toggleCurrentAppSleepMode(self, gesture):
262262
def script_reportCurrentLine(self, gesture):
263263
obj = api.getFocusObject()
264264
treeInterceptor = obj.treeInterceptor
265-
useTextInfo: bool = False
266265
if (
267266
isinstance(treeInterceptor, treeInterceptorHandler.DocumentTreeInterceptor)
268267
and not treeInterceptor.passThrough
269268
):
270269
obj = treeInterceptor
271-
useTextInfo = True
272-
else:
273-
useTextInfo = obj._shouldUseTextInfoForReading
274-
if useTextInfo:
275-
try:
276-
info = obj.makeTextInfo(textInfos.POSITION_CARET)
277-
except (NotImplementedError, RuntimeError):
278-
info = obj.makeTextInfo(textInfos.POSITION_FIRST)
279-
else:
280-
info = NVDAObjectTextInfo(obj, textInfos.POSITION_FIRST)
270+
try:
271+
info = obj.makeTextInfo(textInfos.POSITION_CARET)
272+
except (NotImplementedError, RuntimeError):
273+
info = obj.makeTextInfo(textInfos.POSITION_FIRST)
281274
info.expand(textInfos.UNIT_LINE)
282275
scriptCount = getLastScriptRepeatCount()
283276
if scriptCount == 0:

0 commit comments

Comments
 (0)