#1115 Add QR Code for onion URL

This commit is contained in:
Miguel Jacq 2020-05-31 17:44:57 +10:00
parent 1c424500f0
commit 4cf508ed8e
No known key found for this signature in database
GPG Key ID: EEA4341C6D97A0B6
9 changed files with 123 additions and 7 deletions

View File

@ -24,7 +24,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings from onionshare import strings
from ..widgets import Alert from ..widgets import Alert
from ..widgets import Image
from ..widgets import QRCodeDialog
class ServerStatus(QtWidgets.QWidget): class ServerStatus(QtWidgets.QWidget):
""" """
@ -96,6 +97,14 @@ class ServerStatus(QtWidgets.QWidget):
self.copy_hidservauth_button = QtWidgets.QPushButton( self.copy_hidservauth_button = QtWidgets.QPushButton(
strings._("gui_copy_hidservauth") strings._("gui_copy_hidservauth")
) )
self.show_url_qr_code_button = QtWidgets.QPushButton(strings._("gui_show_url_qr_code"))
self.show_url_qr_code_button.hide()
self.show_url_qr_code_button.clicked.connect(self.show_url_qr_code_button_clicked)
self.show_url_qr_code_button.setFlat(True)
self.show_url_qr_code_button.setStyleSheet(
self.common.gui.css["server_status_url_buttons"]
)
self.copy_hidservauth_button.setFlat(True) self.copy_hidservauth_button.setFlat(True)
self.copy_hidservauth_button.setStyleSheet( self.copy_hidservauth_button.setStyleSheet(
self.common.gui.css["server_status_url_buttons"] self.common.gui.css["server_status_url_buttons"]
@ -103,6 +112,7 @@ class ServerStatus(QtWidgets.QWidget):
self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth) self.copy_hidservauth_button.clicked.connect(self.copy_hidservauth)
url_buttons_layout = QtWidgets.QHBoxLayout() url_buttons_layout = QtWidgets.QHBoxLayout()
url_buttons_layout.addWidget(self.copy_url_button) url_buttons_layout.addWidget(self.copy_url_button)
url_buttons_layout.addWidget(self.show_url_qr_code_button)
url_buttons_layout.addWidget(self.copy_hidservauth_button) url_buttons_layout.addWidget(self.copy_hidservauth_button)
url_buttons_layout.addStretch() url_buttons_layout.addStretch()
@ -190,6 +200,8 @@ class ServerStatus(QtWidgets.QWidget):
self.url.show() self.url.show()
self.copy_url_button.show() self.copy_url_button.show()
self.show_url_qr_code_button.show()
if self.settings.get("general", "client_auth"): if self.settings.get("general", "client_auth"):
self.copy_hidservauth_button.show() self.copy_hidservauth_button.show()
else: else:
@ -364,6 +376,12 @@ class ServerStatus(QtWidgets.QWidget):
self.cancel_server() self.cancel_server()
self.button_clicked.emit() self.button_clicked.emit()
def show_url_qr_code_button_clicked(self):
"""
Show a QR code of the onion URL.
"""
self.qr_code_dialog = QRCodeDialog(self.common, self.get_url())
def start_server(self): def start_server(self):
""" """
Start the server. Start the server.

View File

@ -18,7 +18,8 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. along with this program. If not, see <http://www.gnu.org/licenses/>.
""" """
from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5 import QtCore, QtWidgets, QtGui
from onionshare import strings
import qrcode
class Alert(QtWidgets.QMessageBox): class Alert(QtWidgets.QMessageBox):
""" """
@ -90,3 +91,60 @@ class MinimumWidthWidget(QtWidgets.QWidget):
super(MinimumWidthWidget, self).__init__() super(MinimumWidthWidget, self).__init__()
self.setMinimumWidth(width) self.setMinimumWidth(width)
class Image(qrcode.image.base.BaseImage):
def __init__(self, border, width, box_size):
self.border = border
self.width = width
self.box_size = box_size
size = (width + border * 2) * box_size
self._image = QtGui.QImage(
size, size, QtGui.QImage.Format_RGB16)
self._image.fill(QtCore.Qt.white)
def pixmap(self):
return QtGui.QPixmap.fromImage(self._image)
def drawrect(self, row, col):
painter = QtGui.QPainter(self._image)
painter.fillRect(
(col + self.border) * self.box_size,
(row + self.border) * self.box_size,
self.box_size, self.box_size,
QtCore.Qt.black)
def save(self, stream, kind=None):
pass
class QRCodeDialog(QtWidgets.QDialog):
"""
A dialog showing a QR code.
"""
def __init__(self, common, text):
super(QRCodeDialog, self).__init__()
self.common = common
self.text = text
self.common.log("QrCode", "__init__")
self.qr_label = QtWidgets.QLabel(self)
self.qr_label.setPixmap(
qrcode.make(self.text, image_factory=Image).pixmap())
self.qr_label_description = QtWidgets.QLabel(self)
self.qr_label_description.setText(strings._("gui_qr_code_description"))
self.qr_label_description.setWordWrap(True)
self.setWindowTitle(strings._("gui_qr_code_dialog_title"))
self.setWindowIcon(
QtGui.QIcon(self.common.get_resource_path("images/logo.png"))
)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.qr_label)
layout.addWidget(self.qr_label_description)
self.exec_()

30
poetry.lock generated
View File

@ -53,9 +53,9 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
version = "7.1.1" version = "7.1.1"
[[package]] [[package]]
category = "dev" category = "main"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
marker = "sys_platform == \"win32\"" marker = "platform_system == \"Windows\" or sys_platform == \"win32\""
name = "colorama" name = "colorama"
optional = false optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
@ -356,6 +356,24 @@ pytest = ">=3.0.0"
dev = ["pre-commit", "tox"] dev = ["pre-commit", "tox"]
doc = ["sphinx", "sphinx-rtd-theme"] doc = ["sphinx", "sphinx-rtd-theme"]
[[package]]
category = "main"
description = "QR Code image generator"
name = "qrcode"
optional = false
python-versions = "*"
version = "6.1"
[package.dependencies]
colorama = "*"
six = "*"
[package.extras]
dev = ["tox", "pytest", "mock"]
maintainer = ["zest.releaser"]
pil = ["pillow"]
test = ["pytest", "pytest-cov", "mock"]
[[package]] [[package]]
category = "main" category = "main"
description = "Python HTTP for Humans." description = "Python HTTP for Humans."
@ -375,7 +393,7 @@ security = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)"]
socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"] socks = ["PySocks (>=1.5.6,<1.5.7 || >1.5.7)", "win-inet-pton"]
[[package]] [[package]]
category = "dev" category = "main"
description = "Python 2 and 3 compatibility utilities" description = "Python 2 and 3 compatibility utilities"
name = "six" name = "six"
optional = false optional = false
@ -451,7 +469,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"]
testing = ["jaraco.itertools", "func-timeout"] testing = ["jaraco.itertools", "func-timeout"]
[metadata] [metadata]
content-hash = "41d68ea93701fdaa1aa56159195db7a65863e3b34cc7305ef4a3f5d02f2bdf13" content-hash = "3f46cfec01bcb5166c9f354aaf4439064b477955f3ea2373fcfdb65d5b89276e"
python-versions = "^3.7" python-versions = "^3.7"
[metadata.files] [metadata.files]
@ -671,6 +689,10 @@ pytest-qt = [
{file = "pytest-qt-3.3.0.tar.gz", hash = "sha256:714b0bf86c5313413f2d300ac613515db3a1aef595051ab8ba2ffe619dbe8925"}, {file = "pytest-qt-3.3.0.tar.gz", hash = "sha256:714b0bf86c5313413f2d300ac613515db3a1aef595051ab8ba2ffe619dbe8925"},
{file = "pytest_qt-3.3.0-py2.py3-none-any.whl", hash = "sha256:5f8928288f50489d83f5d38caf2d7d9fcd6e7cf769947902caa4661dc7c851e3"}, {file = "pytest_qt-3.3.0-py2.py3-none-any.whl", hash = "sha256:5f8928288f50489d83f5d38caf2d7d9fcd6e7cf769947902caa4661dc7c851e3"},
] ]
qrcode = [
{file = "qrcode-6.1-py2.py3-none-any.whl", hash = "sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5"},
{file = "qrcode-6.1.tar.gz", hash = "sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369"},
]
requests = [ requests = [
{file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"},
{file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"},

View File

@ -30,6 +30,7 @@ urllib3 = "*"
Werkzeug = "*" Werkzeug = "*"
watchdog = "*" watchdog = "*"
psutil = "*" psutil = "*"
qrcode = "^6.1"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
atomicwrites = "*" atomicwrites = "*"

View File

@ -28,6 +28,9 @@
"gui_copied_url": "OnionShare address copied to clipboard", "gui_copied_url": "OnionShare address copied to clipboard",
"gui_copied_hidservauth_title": "Copied HidServAuth", "gui_copied_hidservauth_title": "Copied HidServAuth",
"gui_copied_hidservauth": "HidServAuth line copied to clipboard", "gui_copied_hidservauth": "HidServAuth line copied to clipboard",
"gui_show_url_qr_code": "Show QR code",
"gui_qr_code_dialog_title": "OnionShare QR Code",
"gui_qr_code_description": "Scan this QR code with a QR reader, such as the camera on your phone, in order to share it over an application such as Signal.",
"gui_waiting_to_start": "Scheduled to start in {}. Click to cancel.", "gui_waiting_to_start": "Scheduled to start in {}. Click to cancel.",
"gui_please_wait": "Starting… Click to cancel.", "gui_please_wait": "Starting… Click to cancel.",
"error_rate_limit": "Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.", "error_rate_limit": "Someone has made too many wrong attempts to guess your password, so OnionShare has stopped the server. Start sharing again and send the recipient a new address to share.",

View File

@ -297,6 +297,17 @@ class GuiBaseTest(unittest.TestCase):
f"http://onionshare:{tab.get_mode().server_status.web.password}@127.0.0.1:{tab.app.port}", f"http://onionshare:{tab.get_mode().server_status.web.password}@127.0.0.1:{tab.app.port}",
) )
def have_show_qr_code_button(self, tab):
"""Test that the Show QR Code URL button is shown and that it loads a QR Code Dialog"""
self.assertTrue(tab.get_mode().server_status.show_url_qr_code_button.isVisible())
def accept_dialog():
window = tab.common.gui.qtapp.activeWindow()
if window:
window.close()
QtCore.QTimer.singleShot(500, accept_dialog)
tab.get_mode().server_status.show_url_qr_code_button.click()
def server_status_indicator_says_started(self, tab): def server_status_indicator_says_started(self, tab):
"""Test that the Server Status indicator shows we are started""" """Test that the Server Status indicator shows we are started"""
if type(tab.get_mode()) == ReceiveMode: if type(tab.get_mode()) == ReceiveMode:

View File

@ -112,6 +112,7 @@ class TestReceive(GuiBaseTest):
self.have_a_password(tab) self.have_a_password(tab)
self.url_description_shown(tab) self.url_description_shown(tab)
self.have_copy_url_button(tab) self.have_copy_url_button(tab)
self.have_show_qr_code_button(tab)
self.server_status_indicator_says_started(tab) self.server_status_indicator_says_started(tab)
self.web_page(tab, "Select the files you want to send, then click") self.web_page(tab, "Select the files you want to send, then click")

View File

@ -231,6 +231,7 @@ class TestShare(GuiBaseTest):
self.have_a_password(tab) self.have_a_password(tab)
self.url_description_shown(tab) self.url_description_shown(tab)
self.have_copy_url_button(tab) self.have_copy_url_button(tab)
self.have_show_qr_code_button(tab)
self.server_status_indicator_says_started(tab) self.server_status_indicator_says_started(tab)
def run_all_share_mode_download_tests(self, tab): def run_all_share_mode_download_tests(self, tab):

View File

@ -70,6 +70,7 @@ class TestWebsite(GuiBaseTest):
self.have_a_password(tab) self.have_a_password(tab)
self.url_description_shown(tab) self.url_description_shown(tab)
self.have_copy_url_button(tab) self.have_copy_url_button(tab)
self.have_show_qr_code_button(tab)
self.server_status_indicator_says_started(tab) self.server_status_indicator_says_started(tab)
def run_all_website_mode_download_tests(self, tab): def run_all_website_mode_download_tests(self, tab):