From ca4ab87f7d89b4e8e1aaa28da2f89604a7d504c2 Mon Sep 17 00:00:00 2001 From: mailchuck Date: Mon, 14 Dec 2015 19:43:39 +0100 Subject: [PATCH] HTML detector and switcher HTML messages are detected and if present, the top of the message textedit displays a clickable area that switches HTML rendering on and off. Fixes #13 --- src/bitmessageqt/__init__.py | 4 +- src/bitmessageqt/bitmessageui.py | 7 ++- src/bitmessageqt/messageview.py | 44 ++++++++++++++ src/bitmessageqt/safehtmlparser.py | 97 ++++++++++++++++++++++++++++++ 4 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 src/bitmessageqt/messageview.py create mode 100644 src/bitmessageqt/safehtmlparser.py diff --git a/src/bitmessageqt/__init__.py b/src/bitmessageqt/__init__.py index a699cebc..b73e9718 100644 --- a/src/bitmessageqt/__init__.py +++ b/src/bitmessageqt/__init__.py @@ -35,6 +35,7 @@ from addaddressdialog import * from newsubscriptiondialog import * from regenerateaddresses import * from newchandialog import * +from safehtmlparser import * from specialaddressbehavior import * from emailgateway import * from settings import * @@ -4026,10 +4027,9 @@ class MyForm(settingsmixin.SMainWindow): data = self.getCurrentMessageId() if data != False: message = "Error occurred: could not load message from disk." - message = unicode(message, 'utf-8)') messageTextedit.setCurrentFont(QtGui.QFont()) messageTextedit.setTextColor(QtGui.QColor()) - messageTextedit.setPlainText(message) + messageTextedit.setContent(message) def tableWidgetAddressBookItemChanged(self): currentRow = self.ui.tableWidgetAddressBook.currentRow() diff --git a/src/bitmessageqt/bitmessageui.py b/src/bitmessageqt/bitmessageui.py index e303567f..7a565d1c 100644 --- a/src/bitmessageqt/bitmessageui.py +++ b/src/bitmessageqt/bitmessageui.py @@ -8,6 +8,7 @@ # WARNING! All changes made in this file will be lost! from PyQt4 import QtCore, QtGui +from messageview import MessageView import settingsmixin try: @@ -123,7 +124,7 @@ class Ui_MainWindow(object): self.tableWidgetInbox.verticalHeader().setVisible(False) self.tableWidgetInbox.verticalHeader().setDefaultSectionSize(26) self.verticalSplitter_7.addWidget(self.tableWidgetInbox) - self.textEditInboxMessage = QtGui.QTextEdit(self.inbox) + self.textEditInboxMessage = MessageView(self.inbox) self.textEditInboxMessage.setBaseSize(QtCore.QSize(0, 500)) self.textEditInboxMessage.setReadOnly(True) self.textEditInboxMessage.setObjectName(_fromUtf8("textEditInboxMessage")) @@ -428,7 +429,7 @@ class Ui_MainWindow(object): self.tableWidgetInboxSubscriptions.verticalHeader().setVisible(False) self.tableWidgetInboxSubscriptions.verticalHeader().setDefaultSectionSize(26) self.verticalSplitter_4.addWidget(self.tableWidgetInboxSubscriptions) - self.textEditInboxMessageSubscriptions = QtGui.QTextEdit(self.subscriptions) + self.textEditInboxMessageSubscriptions = MessageView(self.subscriptions) self.textEditInboxMessageSubscriptions.setBaseSize(QtCore.QSize(0, 500)) self.textEditInboxMessageSubscriptions.setReadOnly(True) self.textEditInboxMessageSubscriptions.setObjectName(_fromUtf8("textEditInboxMessageSubscriptions")) @@ -527,7 +528,7 @@ class Ui_MainWindow(object): self.tableWidgetInboxChans.verticalHeader().setVisible(False) self.tableWidgetInboxChans.verticalHeader().setDefaultSectionSize(26) self.verticalSplitter_8.addWidget(self.tableWidgetInboxChans) - self.textEditInboxMessageChans = QtGui.QTextEdit(self.chans) + self.textEditInboxMessageChans = MessageView(self.chans) self.textEditInboxMessageChans.setBaseSize(QtCore.QSize(0, 500)) self.textEditInboxMessageChans.setReadOnly(True) self.textEditInboxMessageChans.setObjectName(_fromUtf8("textEditInboxMessageChans")) diff --git a/src/bitmessageqt/messageview.py b/src/bitmessageqt/messageview.py new file mode 100644 index 00000000..94e73ff1 --- /dev/null +++ b/src/bitmessageqt/messageview.py @@ -0,0 +1,44 @@ +from PyQt4 import QtCore, QtGui + +from safehtmlparser import * + +class MessageView(QtGui.QTextEdit): + MODE_PLAIN = 0 + MODE_HTML = 1 + TEXT_PLAIN = "HTML detected, click here to display" + TEXT_HTML = "Click here to disable HTML" + + def __init__(self, parent = 0): + super(MessageView, self).__init__(parent) + self.mode = MessageView.MODE_PLAIN + self.html = None + + def mousePressEvent(self, event): + #text = textCursor.block().text() + if event.button() == QtCore.Qt.LeftButton and self.html.has_html and self.cursorForPosition(event.pos()).block().blockNumber() == 0: + if self.mode == MessageView.MODE_PLAIN: + self.showHTML() + else: + self.showPlain() + else: + super(MessageView, self).mousePressEvent(event) + + def showPlain(self): + self.mode = MessageView.MODE_PLAIN + out = self.html.raw + if self.html.has_html: + out = "
" + QtGui.QApplication.translate("MessageView", MessageView.TEXT_PLAIN) + "

" + out + self.setHtml(QtCore.QString(out)) + + def showHTML(self): + self.mode = MessageView.MODE_HTML + out = self.html.sanitised + out = "
" + QtGui.QApplication.translate("MessageView", MessageView.TEXT_HTML) + "

" + out + self.setHtml(QtCore.QString(out)) + + def setContent(self, data): + self.html = SafeHTMLParser() + self.html.allow_picture = True + self.html.feed(data) + self.html.close() + self.showPlain() diff --git a/src/bitmessageqt/safehtmlparser.py b/src/bitmessageqt/safehtmlparser.py new file mode 100644 index 00000000..e22a776f --- /dev/null +++ b/src/bitmessageqt/safehtmlparser.py @@ -0,0 +1,97 @@ +from HTMLParser import HTMLParser +import inspect +from urllib import quote, quote_plus + +class SafeHTMLParser(HTMLParser): + # from html5lib.sanitiser + acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', + 'article', 'aside', 'audio', 'b', 'big', 'blockquote', 'br', 'button', + 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', + 'command', 'datagrid', 'datalist', 'dd', 'del', 'details', 'dfn', + 'dialog', 'dir', 'div', 'dl', 'dt', 'em', 'event-source', 'fieldset', + 'figcaption', 'figure', 'footer', 'font', 'header', 'h1', + 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'ins', + 'keygen', 'kbd', 'label', 'legend', 'li', 'm', 'map', 'menu', 'meter', + 'multicol', 'nav', 'nextid', 'ol', 'output', 'optgroup', 'option', + 'p', 'pre', 'progress', 'q', 's', 'samp', 'section', 'select', + 'small', 'sound', 'source', 'spacer', 'span', 'strike', 'strong', + 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'time', 'tfoot', + 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'video'] + def __init__(self, *args, **kwargs): + HTMLParser.__init__(self, *args, **kwargs) + self.elements = set() + self.sanitised = u"" + self.raw = u"" + self.has_html = False + + def reset_safe(self): + self.raw = u"" + self.sanitised = u"" + + def add_if_acceptable(self, tag, attrs = None): + if not tag in self.acceptable_elements: + return + self.sanitised += "<" + if inspect.stack()[1][3] == "handle_endtag": + self.sanitised += "/" + self.sanitised += tag + if attrs is not None: + for attr in attrs: + if tag == "img" and attr[0] == "src" and not self.allow_picture: + attr[1] = "" + self.sanitised += " " + quote_plus(attr[0]) + "=\"" + attr[1] + "\"" + if inspect.stack()[1][3] == "handle_startendtag": + self.sanitised += "/" + self.sanitised += ">" + + def add_raw(self, tag, attrs = None): + self.raw += "<" + if inspect.stack()[1][3] == "handle_endtag": + self.raw += "/" + self.raw += tag + if attrs is not None: + for attr in attrs: + if tag == "img" and attr[0] == "src" and not self.allow_picture: + attr[1] = "" + self.raw += " " + attr[0] + "="" + attr[1] + """ + if inspect.stack()[1][3] == "handle_startendtag": + self.raw += "/" + self.raw += ">" + + def handle_starttag(self, tag, attrs): + if tag in self.acceptable_elements: + self.has_html = True + self.add_if_acceptable(tag, attrs) + self.add_raw(tag, attrs) + + def handle_endtag(self, tag): + self.add_if_acceptable(tag) + self.add_raw(tag) + + def handle_startendtag(self, tag, attrs): + if tag in self.acceptable_elements: + self.has_html = True + self.add_if_acceptable(tag, attrs) + self.add_raw(tag, attrs) + + def handle_data(self, data): + self.sanitised += unicode(data, 'utf-8', 'replace') + tmp = data.replace("\n", "
") + self.raw += unicode(tmp, 'utf-8', 'replace') + + def handle_charref(self, name): + self.sanitised += "&#" + name + ";" + self.raw += quote("&#" + name + ";") + + def handle_entityref(self, name): + self.sanitised += "&" + name + ";" + self.raw += quote("&" + name + ";") + + def is_html(self, text = None, allow_picture = False): + if text: + self.reset() + self.reset_safe() + self.feed(text) + self.close() + self.allow_picture = allow_picture + return self.has_html \ No newline at end of file