From decfd020331c2025f15e0e10da3c9e3e68a2281e Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Thu, 11 Dec 2014 23:34:03 +0100 Subject: [PATCH] Use a QNAM to pastebin from crash dialog. Fixes #280. --- qutebrowser/network/pastebin.py | 101 ++++++++++++++++++++++++++++++++ qutebrowser/utils/utils.py | 32 ---------- qutebrowser/widgets/crash.py | 56 ++++++++++++++---- 3 files changed, 146 insertions(+), 43 deletions(-) create mode 100644 qutebrowser/network/pastebin.py diff --git a/qutebrowser/network/pastebin.py b/qutebrowser/network/pastebin.py new file mode 100644 index 000000000..d32a7d3a8 --- /dev/null +++ b/qutebrowser/network/pastebin.py @@ -0,0 +1,101 @@ +# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et: + +# Copyright 2014 Florian Bruhin (The Compiler) +# +# This file is part of qutebrowser. +# +# qutebrowser is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# qutebrowser is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with qutebrowser. If not, see . + +"""Client for the pastebin.""" + +import functools +import urllib.request +import urllib.parse + +from PyQt5.QtCore import pyqtSignal, QObject, QUrl +from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest, + QNetworkReply) + + +class PastebinClient(QObject): + + """A client for http://p.cmpl.cc/ using QNetworkAccessManager. + + Attributes: + _nam: The QNetworkAccessManager used. + + Class attributes: + API_URL: The base API URL. + + Signals: + success: Emitted when the paste succeeded. + arg: The URL of the paste, as string. + error: Emitted when the paste failed. + arg: The error message, as string. + """ + + API_URL = 'http://paste.the-compiler.org/api/' + success = pyqtSignal(str) + error = pyqtSignal(str) + + def __init__(self, parent=None): + super().__init__(parent) + self._nam = QNetworkAccessManager(self) + + def paste(self, name, title, text, parent=None): + """Paste the text into a pastebin and return the URL. + + Args: + name: The username to post as. + title: The post title. + text: The text to post. + parent: The parent paste to reply to. + """ + data = { + 'text': text, + 'title': title, + 'name': name, + } + if parent is not None: + data['reply'] = parent + encoded_data = urllib.parse.urlencode(data).encode('utf-8') + create_url = urllib.parse.urljoin(self.API_URL, 'create') + request = QNetworkRequest(QUrl(create_url)) + request.setHeader(QNetworkRequest.ContentTypeHeader, + 'application/x-www-form-urlencoded;charset=utf-8') + reply = self._nam.post(request, encoded_data) + if reply.isFinished(): + self.on_reply_finished(reply) + else: + reply.finished.connect(functools.partial( + self.on_reply_finished, reply)) + + def on_reply_finished(self, reply): + """Read the data and finish when the reply finished. + + Args: + reply: The QNetworkReply which finished. + """ + if reply.error() != QNetworkReply.NoError: + self.error.emit(reply.errorString()) + return + try: + url = bytes(reply.readAll()).decode('utf-8') + except UnicodeDecodeError: + self.error.emit("Invalid UTF-8 data received in reply!") + return + if url.startswith('http://'): + self.success.emit(url) + else: + self.error.emit("Invalid data received in reply!") diff --git a/qutebrowser/utils/utils.py b/qutebrowser/utils/utils.py index c10c36cf1..d47920c71 100644 --- a/qutebrowser/utils/utils.py +++ b/qutebrowser/utils/utils.py @@ -24,8 +24,6 @@ import sys import enum import inspect import os.path -import urllib.request -import urllib.parse import collections import functools import contextlib @@ -85,36 +83,6 @@ def read_file(filename): return data.decode('UTF-8') -def pastebin(name, title, text, parent=None): - """Paste the text into a pastebin and return the URL. - - Args: - name: The username to post as. - title: The post title. - text: The text to post. - parent: The parent paste to reply to. - """ - api_url = 'http://paste.the-compiler.org/api/' - data = { - 'text': text, - 'title': title, - 'name': name, - } - if parent is not None: - data['reply'] = parent - encoded_data = urllib.parse.urlencode(data).encode('utf-8') - create_url = urllib.parse.urljoin(api_url, 'create') - headers = { - 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' - } - request = urllib.request.Request(create_url, encoded_data, headers) - response = urllib.request.urlopen(request) - url = response.read().decode('utf-8').rstrip() - if not url.startswith('http'): - raise ValueError("Got unexpected response: {}".format(url)) - return url - - def actute_warning(): """Display a warning about the dead_actute issue if needed.""" # WORKAROUND (remove this when we bump the requirements to 5.3.0) diff --git a/qutebrowser/widgets/crash.py b/qutebrowser/widgets/crash.py index 901f4ce4e..b21b8eb85 100644 --- a/qutebrowser/widgets/crash.py +++ b/qutebrowser/widgets/crash.py @@ -33,6 +33,7 @@ from PyQt5.QtWidgets import (QDialog, QLabel, QTextEdit, QPushButton, from qutebrowser.utils import version, log, utils, objreg from qutebrowser.widgets.misc import DetailFold +from qutebrowser.network import pastebin class _CrashDialog(QDialog): @@ -50,6 +51,9 @@ class _CrashDialog(QDialog): _hbox: The QHboxLayout containing the buttons _url: Pastebin URL QLabel. _crash_info: A list of tuples with title and crash information. + _paste_client: A PastebinClient instance to use. + _paste_text: The text to pastebin. + _resolution: Whether the dialog should be accepted on close. """ NAME = None @@ -68,9 +72,12 @@ class _CrashDialog(QDialog): self._hbox = None self._lbl = None self._chk_report = None + self._resolution = None + self._paste_text = None self.setWindowTitle("Whoops!") self.resize(QSize(640, 600)) self._vbox = QVBoxLayout(self) + self._paste_client = pastebin.PastebinClient(self) self._init_text() info = QLabel("What were you doing when this crash/bug happened?") @@ -179,20 +186,20 @@ class _CrashDialog(QDialog): lines.append(self._contact.toPlainText()) lines.append("========== Debug log ==========") lines.append(self._debug_log.toPlainText()) - text = '\n\n'.join(lines) + self._paste_text = '\n\n'.join(lines) try: user = getpass.getuser() except Exception as e: log.misc.exception("Error while getting user") user = 'unknown' try: - utils.pastebin(user, "qutebrowser {}".format(self.NAME), text, - parent='90286958') # http://p.cmpl.cc/90286958 + # parent: http://p.cmpl.cc/90286958 + self._paste_client.paste(user, "qutebrowser {}".format(self.NAME), + self._paste_text, parent='90286958') except Exception as e: log.misc.exception("Error while paste-binning") exc_text = '{}: {}'.format(e.__class__.__name__, e) - error_dlg = ReportErrorDialog(exc_text, text, self) - error_dlg.exec_() + self.show_error(exc_text) @pyqtSlot() def on_button_clicked(self, button, accept): @@ -200,18 +207,44 @@ class _CrashDialog(QDialog): button.setText("Reporting...") for btn in self._buttons: btn.setEnabled(False) - self.hide() - self.maybe_report() - if accept: + self._resolution = accept + self._paste_client.success.connect(self.finish) + self._paste_client.error.connect(self.show_error) + reported = self.maybe_report() + if not reported: + self.finish() + + @pyqtSlot(str) + def show_error(self, text): + """Show a paste error dialog. + + Args: + text: The paste text to show. + """ + error_dlg = ReportErrorDialog(text, self._paste_text, self) + error_dlg.finished.connect(self.finish) + error_dlg.show() + + @pyqtSlot() + def finish(self): + """Accept/reject the dialog when reporting is done.""" + if self._resolution: self.accept() else: self.reject() @pyqtSlot() def maybe_report(self): - """Report the bug if the user allowed us to.""" + """Report the bug if the user allowed us to. + + Return: + True if a report was done, False otherwise. + """ if self._chk_report.isChecked(): self.report() + return True + else: + return False class ExceptionCrashDialog(_CrashDialog): @@ -371,8 +404,8 @@ class ReportDialog(_CrashDialog): def _init_buttons(self): super()._init_buttons() self._btn_report = QPushButton("Report", default=True) - self._btn_report.clicked.connect(self.report) - self._btn_report.clicked.connect(self.close) + self._btn_report.clicked.connect( + functools.partial(self.on_button_clicked, self._btn_report, True)) self._hbox.addWidget(self._btn_report) def _init_checkboxes(self, _debug): @@ -400,6 +433,7 @@ class ReportDialog(_CrashDialog): report, which would be pretty useless without this info. """ self.report() + return True class ReportErrorDialog(QDialog):