From 4cf508ed8e05fc38762ca49942d3a83a3d8123f5 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Sun, 31 May 2020 17:44:57 +1000 Subject: [PATCH] #1115 Add QR Code for onion URL --- onionshare_gui/tab/server_status.py | 20 +++++++++- onionshare_gui/widgets.py | 60 ++++++++++++++++++++++++++++- poetry.lock | 30 +++++++++++++-- pyproject.toml | 1 + share/locale/en.json | 5 ++- tests/gui_base_test.py | 11 ++++++ tests/test_gui_receive.py | 1 + tests/test_gui_share.py | 1 + tests/test_gui_website.py | 1 + 9 files changed, 123 insertions(+), 7 deletions(-) diff --git a/onionshare_gui/tab/server_status.py b/onionshare_gui/tab/server_status.py index 0fbc11b6..10d45bcc 100644 --- a/onionshare_gui/tab/server_status.py +++ b/onionshare_gui/tab/server_status.py @@ -24,7 +24,8 @@ from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings from ..widgets import Alert - +from ..widgets import Image +from ..widgets import QRCodeDialog class ServerStatus(QtWidgets.QWidget): """ @@ -96,6 +97,14 @@ class ServerStatus(QtWidgets.QWidget): self.copy_hidservauth_button = QtWidgets.QPushButton( 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.setStyleSheet( 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) url_buttons_layout = QtWidgets.QHBoxLayout() 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.addStretch() @@ -190,6 +200,8 @@ class ServerStatus(QtWidgets.QWidget): self.url.show() self.copy_url_button.show() + self.show_url_qr_code_button.show() + if self.settings.get("general", "client_auth"): self.copy_hidservauth_button.show() else: @@ -364,6 +376,12 @@ class ServerStatus(QtWidgets.QWidget): self.cancel_server() 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): """ Start the server. diff --git a/onionshare_gui/widgets.py b/onionshare_gui/widgets.py index 74ef2c88..3b1180b3 100644 --- a/onionshare_gui/widgets.py +++ b/onionshare_gui/widgets.py @@ -18,7 +18,8 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ from PyQt5 import QtCore, QtWidgets, QtGui - +from onionshare import strings +import qrcode class Alert(QtWidgets.QMessageBox): """ @@ -90,3 +91,60 @@ class MinimumWidthWidget(QtWidgets.QWidget): super(MinimumWidthWidget, self).__init__() 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_() + diff --git a/poetry.lock b/poetry.lock index 7c4603ba..b3ac93b3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -53,9 +53,9 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" version = "7.1.1" [[package]] -category = "dev" +category = "main" description = "Cross-platform colored terminal text." -marker = "sys_platform == \"win32\"" +marker = "platform_system == \"Windows\" or sys_platform == \"win32\"" name = "colorama" optional = false 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"] 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]] category = "main" 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"] [[package]] -category = "dev" +category = "main" description = "Python 2 and 3 compatibility utilities" name = "six" optional = false @@ -451,7 +469,7 @@ docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] testing = ["jaraco.itertools", "func-timeout"] [metadata] -content-hash = "41d68ea93701fdaa1aa56159195db7a65863e3b34cc7305ef4a3f5d02f2bdf13" +content-hash = "3f46cfec01bcb5166c9f354aaf4439064b477955f3ea2373fcfdb65d5b89276e" python-versions = "^3.7" [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-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 = [ {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, {file = "requests-2.23.0.tar.gz", hash = "sha256:b3f43d496c6daba4493e7c431722aeb7dbc6288f52a6e04e7b6023b0247817e6"}, diff --git a/pyproject.toml b/pyproject.toml index 53ce5858..411d03f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ urllib3 = "*" Werkzeug = "*" watchdog = "*" psutil = "*" +qrcode = "^6.1" [tool.poetry.dev-dependencies] atomicwrites = "*" diff --git a/share/locale/en.json b/share/locale/en.json index cca7d92e..57491c4b 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -28,6 +28,9 @@ "gui_copied_url": "OnionShare address copied to clipboard", "gui_copied_hidservauth_title": "Copied HidServAuth", "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_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.", @@ -205,4 +208,4 @@ "mode_settings_receive_data_dir_label": "Save files to", "mode_settings_receive_data_dir_browse_button": "Browse", "mode_settings_website_disable_csp_checkbox": "Disable Content Security Policy header (allows your website to use third-party resources)" -} \ No newline at end of file +} diff --git a/tests/gui_base_test.py b/tests/gui_base_test.py index 87353cd7..f1a4efac 100644 --- a/tests/gui_base_test.py +++ b/tests/gui_base_test.py @@ -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}", ) + 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): """Test that the Server Status indicator shows we are started""" if type(tab.get_mode()) == ReceiveMode: diff --git a/tests/test_gui_receive.py b/tests/test_gui_receive.py index 4ee0abd8..bd9cf491 100644 --- a/tests/test_gui_receive.py +++ b/tests/test_gui_receive.py @@ -112,6 +112,7 @@ class TestReceive(GuiBaseTest): self.have_a_password(tab) self.url_description_shown(tab) self.have_copy_url_button(tab) + self.have_show_qr_code_button(tab) self.server_status_indicator_says_started(tab) self.web_page(tab, "Select the files you want to send, then click") diff --git a/tests/test_gui_share.py b/tests/test_gui_share.py index c8b6292a..8e814248 100644 --- a/tests/test_gui_share.py +++ b/tests/test_gui_share.py @@ -231,6 +231,7 @@ class TestShare(GuiBaseTest): self.have_a_password(tab) self.url_description_shown(tab) self.have_copy_url_button(tab) + self.have_show_qr_code_button(tab) self.server_status_indicator_says_started(tab) def run_all_share_mode_download_tests(self, tab): diff --git a/tests/test_gui_website.py b/tests/test_gui_website.py index c88a4910..bc6e16ea 100644 --- a/tests/test_gui_website.py +++ b/tests/test_gui_website.py @@ -70,6 +70,7 @@ class TestWebsite(GuiBaseTest): self.have_a_password(tab) self.url_description_shown(tab) self.have_copy_url_button(tab) + self.have_show_qr_code_button(tab) self.server_status_indicator_says_started(tab) def run_all_website_mode_download_tests(self, tab):