Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions contrib/android/p4a_recipes/qt6/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,9 @@
class Qt6RecipePinned(util.InheritedRecipeMixin, Qt6Recipe):
sha512sum = "bf1a1d42d57b4d2e77f7227f4cbe01e847fd65035461b89481063b32f25a57be6e5a07889acc4af65ca9ff9d27b7fe63bd2fe60b8aa7fa19d554394d799fbaa1"

patches = Qt6Recipe.patches + [
os.path.join(os.path.dirname(__file__), "patches", "qt-6-10-rich-text-should-be-opt-in.patch"),
]


recipe = Qt6RecipePinned()
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Set default textFormat of Labels and all other controls to PlainText.
# Security-by-default, instead of convenience: require programmer to opt-in to RichText.

--- a/qtdeclarative/src/quick/items/qquicktext.cpp
+++ b/qtdeclarative/src/quick/items/qquicktext.cpp
@@ -52,7 +52,7 @@ QQuickTextPrivate::QQuickTextPrivate()
, color(0xFF000000), linkColor(0xFF0000FF), styleColor(0xFF000000)
, lineCount(1), multilengthEos(-1)
, elideMode(QQuickText::ElideNone), hAlign(QQuickText::AlignLeft), vAlign(QQuickText::AlignTop)
- , format(QQuickText::AutoText), wrapMode(QQuickText::NoWrap)
+ , format(QQuickText::PlainText), wrapMode(QQuickText::NoWrap)
, style(QQuickText::Normal)
, renderType(QQuickTextUtil::textRenderType<QQuickText>())
, updateType(UpdatePaintNode)
15 changes: 15 additions & 0 deletions electrum/gui/messages.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import textwrap

from electrum.i18n import _
from electrum.submarine_swaps import MIN_FINAL_CLTV_DELTA_FOR_CLIENT

Expand All @@ -6,6 +8,19 @@ def to_rtf(msg):
return '\n'.join(['<p>' + x + '</p>' for x in msg.split('\n\n')])


def wrap_multi_paragraph_text(text: str) -> str:
"""Word-wrap long lines.

- If text contains multiple paragraphs, the paragraph-separation is kept.
- Useful for tooltips (shown on mouse-over), as Qt otherwise
only word-wraps lines longer than the screen-width.
"""
return "\n".join(
textwrap.fill(line)
for line in text.split("\n")
)


MSG_COOPERATIVE_CLOSE = _(
"""Your node will negotiate the transaction fee with the remote node. This method of closing the channel usually results in the lowest fees."""
)
Expand Down
1 change: 1 addition & 0 deletions electrum/gui/qml/components/ExceptionDialog.qml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ ElDialog
text: reportText
wrapMode: Text.Wrap
width: parent.width
textFormat: Text.RichText
}
}
onClosed: destroy()
Expand Down
1 change: 1 addition & 0 deletions electrum/gui/qml/components/OpenWalletDialog.qml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ ElDialog {
text: Daemon.singlePasswordEnabled || isStartup
? qsTr('Please enter password')
: qsTr('Wallet <b>%1</b> requires password to unlock').arg(name)
textFormat: Text.RichText
compact: true
iconStyle: InfoTextArea.IconStyle.Info
backgroundColor: constants.darkerDialogBackground
Expand Down
1 change: 1 addition & 0 deletions electrum/gui/qml/components/Preferences.qml
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ Pane {
Label {
Layout.fillWidth: true
text: qsTr('<b>%1%</b> of payment').arg(maxfeeslider._fees[maxfeeslider.value]/10000)
textFormat: Text.RichText
wrapMode: Text.Wrap
}

Expand Down
1 change: 1 addition & 0 deletions electrum/gui/qml/components/controls/TxInput.qml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ TextHighlightPane {
: '&lt;' + qsTr('unknown amount') + '&gt;'
font.pixelSize: constants.fontSizeMedium
font.family: FixedFont
textFormat: Text.RichText
}
Label {
text: Config.baseUnit
Expand Down
18 changes: 18 additions & 0 deletions electrum/gui/qml/components/main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -929,4 +929,22 @@ ApplicationWindow
property var _lastActive: 0 // record time of last activity
property bool _lockDialogShown: false

// We want all Text/Label/etc components to use PlainText by default.
// Qt normally defaults to AutoText, which allows rich text.
// For our Android builds, we patch Qt at compile-time to change this.
// (see "qt-6-10-rich-text-should-be-opt-in.patch")
// FIXME other platforms? (e.g. running QML on desktop Linux / dev environment)
// This runtime check here aims to prevent regressions by hard-failing.
Label {
id: richtext_sanity_label
Component.onCompleted: {
if (richtext_sanity_label.textFormat !== 0 && AppController.isAndroid()) {
console.log(
"richtext_sanity_label failed check: expected PlainText, "
+ "got " + richtext_sanity_label.textFormat + ". Exiting...")
Qt.callLater(Qt.quit)
}
}
}

}
1 change: 1 addition & 0 deletions electrum/gui/qml/components/wizard/WCCreateSeed.qml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ WizardComponent {
Layout.fillWidth: true
backgroundColor: constants.darkerDialogBackground
iconStyle: InfoTextArea.IconStyle.Warn
textFormat: Text.RichText
}

Label {
Expand Down
1 change: 1 addition & 0 deletions electrum/gui/qml/components/wizard/WCEnterExt.qml
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ WizardComponent {
'<br/>',
qsTr('Do not enable it unless you know what it does!'),
].join(' ')
textFormat: Text.RichText
}

ElCheckBox {
Expand Down
40 changes: 35 additions & 5 deletions electrum/gui/qt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
"you may try 'sudo apt-get install python3-pyqt6'") from e

from PyQt6.QtGui import QGuiApplication, QCursor
from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QWidget, QMenu, QMessageBox, QDialog, QToolTip
from PyQt6.QtCore import QObject, pyqtSignal, QTimer, Qt
from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QWidget, QMenu, QMessageBox, QDialog, QToolTip, QLabel
from PyQt6.QtCore import QObject, pyqtSignal, QTimer, Qt, QEvent

import PyQt6.QtCore as QtCore

Expand Down Expand Up @@ -112,9 +112,6 @@ def eventFilter(self, obj, event):


class ScreenshotProtectionEventFilter(QObject):
def __init__(self):
super().__init__()

def eventFilter(self, obj, event):
if (
event.type() == QtCore.QEvent.Type.Show
Expand All @@ -125,6 +122,36 @@ def eventFilter(self, obj, event):
return False


class InjectNoRichTextEventFilter(QObject):
"""Set the default textFormat of all QLabels to PlainText.

note: this also affects e.g. QMessageBox as it uses a QLabel internally.
FIXME if obj is a QLabel and it contains rich text, has the rich text already been parsed
and acted upon by the time the Polish event is emitted? For example, if the rich text
contains and embedded base64-encoded PNG and there is a vuln in the PNG parser,
is it already too late?
E.g. apparently if using a screen reader, the accessibility bridge queries the label’s text
from inside setText(), and Qt parses the HTML at that moment. So "Polish" is too late there.
In general against parser vulns, it is not even safe to pass untrusted text to the Label()
constructor... Instead:
lbl = Label(); lbl.setTextFormat(Qt.TextFormat.PlainText); lbl.setText(untrusted_text);
should be used... :/
"""
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
if event.type() != QEvent.Type.Polish:
# see https://doc.qt.io/qt-6/qstyle.html#polish :
# > This function [QStyle.polish()] is called for every widget at some point after
# > it has been fully created but just before it is shown for the very first time.
return False
Comment thread
SomberNight marked this conversation as resolved.
if not isinstance(obj, QLabel):
return False
if obj.textFormat() != Qt.TextFormat.AutoText:
# non-default textFormat => we leave it alone
return False
obj.setTextFormat(Qt.TextFormat.PlainText)
return False


class QElectrumApplication(QApplication):
new_window_signal = pyqtSignal(str, object)
quit_signal = pyqtSignal()
Expand Down Expand Up @@ -162,6 +189,8 @@ def __init__(self, *, config: 'SimpleConfig', daemon: 'Daemon', plugins: 'Plugin
self.screenshot_protection_efilter = ScreenshotProtectionEventFilter()
if sys.platform in ['win32', 'windows'] and self.config.GUI_QT_SCREENSHOT_PROTECTION:
self.app.installEventFilter(self.screenshot_protection_efilter)
self.efilter_no_rich_text = InjectNoRichTextEventFilter()
self.app.installEventFilter(self.efilter_no_rich_text)
# explicitly set 'AA_DontShowIconsInMenus' False so menu icons are shown on MacOS
self.app.setAttribute(Qt.ApplicationAttribute.AA_DontShowIconsInMenus, on=False)
self.app.setWindowIcon(read_QIcon("electrum.png"))
Expand Down Expand Up @@ -626,6 +655,7 @@ def standalone_exception_dialog(exception: Union[str, BaseException]) -> None:
app = QApplication([])

msg_box = QMessageBox()
msg_box.setTextFormat(Qt.TextFormat.PlainText)
msg_box.setWindowTitle(_("Error starting Electrum"))
msg_box.setIcon(QMessageBox.Icon.Critical)
msg_box.setText(_("An error occurred") + ":")
Expand Down
2 changes: 2 additions & 0 deletions electrum/gui/qt/channel_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import PyQt6.QtGui as QtGui
import PyQt6.QtWidgets as QtWidgets
import PyQt6.QtCore as QtCore
from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QLabel, QHBoxLayout

from electrum.util import ShortID
Expand Down Expand Up @@ -33,6 +34,7 @@ class LinkedLabel(QtWidgets.QLabel):
def __init__(self, text, on_clicked):
super().__init__(text)
self.linkActivated.connect(on_clicked)
self.setTextFormat(Qt.TextFormat.RichText)


class ChannelDetailsDialog(QtWidgets.QDialog, MessageBoxMixin, QtEventListener):
Expand Down
1 change: 1 addition & 0 deletions electrum/gui/qt/console.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ def __init__(self, text, parent):
self.setMargin(0)
parent.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.setWordWrap(True)
self.setTextFormat(Qt.TextFormat.RichText)

def mousePressEvent(self, e):
self.hide()
Expand Down
3 changes: 2 additions & 1 deletion electrum/gui/qt/exception_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def __init__(self, config: 'SimpleConfig', exctype, value, tb):
main_box = QVBoxLayout()

heading = QLabel('<h2>' + BaseCrashReporter.CRASH_TITLE + '</h2>')
heading.setTextFormat(Qt.TextFormat.RichText)
main_box.addWidget(heading)
main_box.addWidget(QLabel(BaseCrashReporter.CRASH_MESSAGE))

Expand Down Expand Up @@ -210,7 +211,7 @@ def __init__(self, *, parent: QWidget, text: str):

report_text = QLabel(text)
report_text.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
report_text.setTextFormat(Qt.TextFormat.AutoText) # likely rich text
report_text.setTextFormat(Qt.TextFormat.RichText)

scroll_area.setWidget(report_text)
vbox.addWidget(scroll_area)
4 changes: 3 additions & 1 deletion electrum/gui/qt/history_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,7 +627,9 @@ def show_summary(self):
d.setMinimumSize(600, 150)
vbox = QVBoxLayout()
msg = messages.to_rtf(messages.MSG_CAPITAL_GAINS)
vbox.addWidget(WWLabel(msg))
lbl = WWLabel(msg)
lbl.setTextFormat(Qt.TextFormat.RichText)
vbox.addWidget(lbl)
grid = QGridLayout()
grid.addWidget(QLabel(_("Begin")), 0, 1)
grid.addWidget(QLabel(_("End")), 0, 2)
Expand Down
2 changes: 1 addition & 1 deletion electrum/gui/qt/my_treeview.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def addConfig(
checked = bool(configvar.get())
tooltip = None
if (long_desc := configvar.get_long_desc()) is not None:
tooltip = messages.to_rtf(long_desc)
tooltip = messages.wrap_multi_paragraph_text(long_desc)
return self.addToggle(
short_desc,
lambda: self._do_toggle_config(configvar, callback=callback),
Expand Down
2 changes: 1 addition & 1 deletion electrum/gui/qt/network_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ def __init__(self, network: Network, parent=None):
</ul>
"""
)
grid.addWidget(HelpButton(msg), 0, 4)
grid.addWidget(HelpButton(msg, rich_text=True), 0, 4)
grid.addWidget(self.connect_combo, 0, 1, 1, 3)

self.server_e = QLineEdit()
Expand Down
3 changes: 2 additions & 1 deletion electrum/gui/qt/password_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ def __init__(self, msg, kind, OK_button, wallet=None):
# Password Strength Label
if kind != PW_PASSPHRASE:
self.pw_strength = QLabel()
self.pw_strength.setTextFormat(Qt.TextFormat.RichText)
grid.addWidget(self.pw_strength, 3, 0, 1, 2)
self.new_pw.textChanged.connect(self.pw_changed)

Expand Down Expand Up @@ -293,7 +294,7 @@ def __init__(self, parent=None, msg=None):
msg = msg or _('Please enter your password')
WindowModalDialog.__init__(self, parent, _("Enter Password"))
self.pw = pw = PasswordLineEdit()
label = QLabel(msg)
self.label = label = QLabel(msg)
label.setWordWrap(True)
vbox = QVBoxLayout()
vbox.addWidget(label)
Expand Down
4 changes: 2 additions & 2 deletions electrum/gui/qt/plugins_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def __init__(self, name, metadata, status_button: Optional['PluginStatusButton']
name_label.setIcon(icon)
vbox.addWidget(name_label)
vbox.addStretch()
vbox.addWidget(WWLabel(description))
vbox.addWidget(WWLabel(description)) # must be plain text: don't parse untrusted text as rich-text
vbox.addStretch()
form = QFormLayout(None)
if author:
Expand Down Expand Up @@ -196,7 +196,7 @@ def get_plugins_privkey(self) -> Optional['ECPrivkey']:
self.init_plugins_password()
return None
# ask for url and password, same window
pw = self.password_dialog(msg=messages.MSG_THIRD_PARTY_PLUGIN_WARNING)
pw = self.password_dialog(msg=messages.MSG_THIRD_PARTY_PLUGIN_WARNING, rich_text=True)
if not pw:
return None
privkey = self.plugins.derive_privkey(pw, salt)
Expand Down
2 changes: 2 additions & 0 deletions electrum/gui/qt/seed_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,10 @@ def __init__(

vbox.addStretch(1)
self.seed_status = WWLabel('')
self.seed_status.setTextFormat(Qt.TextFormat.RichText)
vbox.addWidget(self.seed_status)
self.seed_warning = WWLabel('')
self.seed_warning.setTextFormat(Qt.TextFormat.RichText)
if msg:
self.seed_warning.setText(seed_warning_msg(seed))
else:
Expand Down
3 changes: 2 additions & 1 deletion electrum/gui/qt/settings_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def checkbox_from_configvar(cv: 'ConfigVarWithConfig') -> QCheckBox:
assert short_desc is not None, f"short_desc missing for {cv}"
cb = QCheckBox(short_desc)
if (long_desc := cv.get_long_desc()) is not None:
cb.setToolTip(messages.to_rtf(long_desc))
long_desc = messages.wrap_multi_paragraph_text(long_desc)
cb.setToolTip(long_desc)
return cb


Expand Down
2 changes: 2 additions & 0 deletions electrum/gui/qt/transaction_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,7 @@ def add_tx_stats(self, vbox):

fee_hbox = QHBoxLayout()
self.fee_label = TxDetailLabel()
self.fee_label.setTextFormat(Qt.TextFormat.RichText)
fee_hbox.addWidget(self.fee_label)
self.fee_warning_icon = QLabel()
pixmap = QPixmap(icon_path("warning"))
Expand Down Expand Up @@ -1121,6 +1122,7 @@ def __init__(
font.setPointSize(font.pointSize() - 1)
self.legend_label.setFont(font)
self.legend_label.setVisible(False)
self.legend_label.setTextFormat(Qt.TextFormat.RichText)
self.text_char_format = QTextCharFormat()
self.text_char_format.setBackground(QBrush(self.color))
self.text_char_format.setToolTip(tooltip)
Expand Down
2 changes: 2 additions & 0 deletions electrum/gui/qt/update_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ def __init__(self, *, latest_version=None):
self.content.setContentsMargins(*[10]*4)

self.heading_label = QLabel()
self.heading_label.setTextFormat(Qt.TextFormat.RichText)
self.content.addWidget(self.heading_label)

self.detail_label = QLabel()
self.detail_label.setTextFormat(Qt.TextFormat.RichText)
self.detail_label.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse)
self.detail_label.setOpenExternalLinks(True)
self.content.addWidget(self.detail_label)
Expand Down
Loading