From 93d27cbb5f49085dd5a7f5e05f2cc45cc84f94a4 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Aug 2015 12:08:36 +0200 Subject: [PATCH 1/8] Escape 0x00 in javascript_escape(). This is needed in older PyQt-versions. --- qutebrowser/browser/webelem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qutebrowser/browser/webelem.py b/qutebrowser/browser/webelem.py index 8ad837ee9..292570c57 100644 --- a/qutebrowser/browser/webelem.py +++ b/qutebrowser/browser/webelem.py @@ -307,6 +307,7 @@ def javascript_escape(text): ('"', r'\"'), # (note it won't hurt when we escape the wrong one). ('\n', r'\n'), # We also need to escape newlines for some reason. ('\r', r'\r'), + ('\x00', r'\x00'), ) for orig, repl in replacements: text = text.replace(orig, repl) From 0ce9ae070c2a6578b6233508be2a3df35dcbd425 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Aug 2015 12:07:49 +0200 Subject: [PATCH 2/8] Add some more test cases for TestJavascriptEscape. --- tests/browser/test_webelem.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/browser/test_webelem.py b/tests/browser/test_webelem.py index 5051aadfd..d88bbc6aa 100644 --- a/tests/browser/test_webelem.py +++ b/tests/browser/test_webelem.py @@ -513,6 +513,11 @@ class TestJavascriptEscape: "foo'bar": r"foo\'bar", 'foo"bar': r'foo\"bar', 'one\\two\rthree\nfour\'five"six': r'one\\two\rthree\nfour\'five\"six', + '\x00': r'\x00', + 'hellö': 'hellö', + '☃': '☃', + '\x80Ā': '\x80Ā', + '𐀀\x00𐀀\x00': r'𐀀\x00𐀀\x00', } @pytest.mark.parametrize('before, after', TESTS.items()) From 0a16f29bd1fae91abe36fca1e5aa2c2785178693 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Aug 2015 08:49:42 +0200 Subject: [PATCH 3/8] Hexlify strings in TestJavascriptEscape. --- tests/browser/test_webelem.py | 60 ++++++++++++++++++--- tests/browser/test_webelem_jsescape.html | 68 ++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 tests/browser/test_webelem_jsescape.html diff --git a/tests/browser/test_webelem.py b/tests/browser/test_webelem.py index d88bbc6aa..72ab7274f 100644 --- a/tests/browser/test_webelem.py +++ b/tests/browser/test_webelem.py @@ -25,10 +25,12 @@ from unittest import mock import collections.abc import operator import itertools +import binascii +import os.path import hypothesis import hypothesis.strategies -from PyQt5.QtCore import QRect, QPoint +from PyQt5.QtCore import PYQT_VERSION, QRect, QPoint from PyQt5.QtWebKit import QWebElement import pytest @@ -525,21 +527,63 @@ class TestJavascriptEscape: """Test javascript escaping with some expected outcomes.""" assert webelem.javascript_escape(before) == after - def _test_escape(self, text, webframe): + def _test_escape(self, text, qtbot, webframe): """Helper function for test_real_escape*.""" + try: + self._test_escape_simple(text, webframe) + except AssertionError: + # Try another method if the simple method failed. + # + # See _test_escape_hexlified documentation on why this is + # necessary. + self._test_escape_hexlified(text, qtbot, webframe) + + def _test_escape_hexlified(self, text, qtbot, webframe): + """Test conversion by hexlifying in javascript. + + Since the conversion of QStrings to Python strings is broken in some + older PyQt versions in some corner cases, we load a HTML file which + generates an MD5 of the escaped text and use that for comparisons. + """ escaped = webelem.javascript_escape(text) - webframe.evaluateJavaScript('window.foo = "{}";'.format(escaped)) - assert webframe.evaluateJavaScript('window.foo') == text + path = os.path.join(os.path.dirname(__file__), + 'test_webelem_jsescape.html') + with open(path, encoding='utf-8') as f: + html_source = f.read().replace('%INPUT%', escaped) + + with qtbot.waitSignal(webframe.loadFinished, raising=True): + webframe.setHtml(html_source) + + result = webframe.evaluateJavaScript('window.qute_test_result') + assert result is not None + assert '|' in result + result_md5, result_text = result.split('|', maxsplit=1) + text_md5 = binascii.hexlify(text.encode('utf-8')).decode('ascii') + assert result_md5 == text_md5, result_text + + def _test_escape_simple(self, text, webframe): + """Test conversion by using evaluateJavaScript.""" + escaped = webelem.javascript_escape(text) + result = webframe.evaluateJavaScript('"{}";'.format(escaped)) + assert result == text @pytest.mark.parametrize('text', TESTS) - def test_real_escape(self, webframe, text): + def test_real_escape(self, webframe, qtbot, text): """Test javascript escaping with a real QWebPage.""" - self._test_escape(text, webframe) + self._test_escape(text, qtbot, webframe) @hypothesis.given(hypothesis.strategies.text()) - def test_real_escape_hypothesis(self, webframe, text): + def test_real_escape_hypothesis(self, webframe, qtbot, text): """Test javascript escaping with a real QWebPage and hypothesis.""" - self._test_escape(text, webframe) + # We can't simply use self._test_escape because of this: + # https://github.com/pytest-dev/pytest-qt/issues/69 + + # self._test_escape(text, qtbot, webframe) + try: + self._test_escape_simple(text, webframe) + except AssertionError: + if PYQT_VERSION >= 0x050300: + self._test_escape_hexlified(text, qtbot, webframe) class TestGetChildFrames: diff --git a/tests/browser/test_webelem_jsescape.html b/tests/browser/test_webelem_jsescape.html new file mode 100644 index 000000000..937499258 --- /dev/null +++ b/tests/browser/test_webelem_jsescape.html @@ -0,0 +1,68 @@ + + + + + + + +

set_text() not called...

+ From d59fa24fd5d3a76af022bc00149baed2d352c96b Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Aug 2015 23:16:30 +0200 Subject: [PATCH 4/8] Update README for Qt 5.5. --- README.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.asciidoc b/README.asciidoc index 25dfd7048..7d5431fdd 100644 --- a/README.asciidoc +++ b/README.asciidoc @@ -89,10 +89,10 @@ Requirements The following software and libraries are required to run qutebrowser: * http://www.python.org/[Python] 3.4 -* http://qt.io/[Qt] 5.2.0 or newer (5.4.2 recommended) +* http://qt.io/[Qt] 5.2.0 or newer (5.5.0 recommended) * QtWebKit * http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer -(5.4.2 recommended) for Python 3 +(5.5.0 recommended) for Python 3 * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools] * http://fdik.org/pyPEG/[pyPEG2] * http://jinja.pocoo.org/[jinja2] From 15e854237e6a583a44e4147be6dff7beb1ab13a8 Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Tue, 4 Aug 2015 23:50:43 +0200 Subject: [PATCH 5/8] Fix exception on ":set -p foo bar!". --- qutebrowser/config/config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 378b3b5bd..e88ec93fd 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -674,10 +674,11 @@ class ConfigManager(QObject): else: try: if option.endswith('!') and value is None: - val = self.get(section_, option[:-1]) + option = option[:-1] + val = self.get(section_, option) layer = 'temp' if temp else 'conf' if isinstance(val, bool): - self.set(layer, section_, option[:-1], str(not val)) + self.set(layer, section_, option, str(not val)) else: raise cmdexc.CommandError( "set: Attempted inversion of non-boolean value.") From 5a25f0b98b60d86eb46d54c47046228be0c20c1c Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Aug 2015 06:55:39 +0200 Subject: [PATCH 6/8] Don't crash on :completion-item-del with no item. If :completion-item-del was invoked with no item selected (e.g. directly after pressing 'o'), there was a crash because the currentIndex was invalid. /cc @antoyo (but I believe one of my changes on top of yours caused this) --- qutebrowser/completion/completer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qutebrowser/completion/completer.py b/qutebrowser/completion/completer.py index bc1a9daa0..2df964658 100644 --- a/qutebrowser/completion/completer.py +++ b/qutebrowser/completion/completer.py @@ -488,6 +488,8 @@ class Completer(QObject): """Delete the current completion item.""" completion = objreg.get('completion', scope='window', window=self._win_id) + if not completion.currentIndex().isValid(): + raise cmdexc.CommandError("No item selected!") try: self.model().srcmodel.delete_cur_item(completion) except NotImplementedError: From 4314b96512a371a6d550682ddb896dbfc520bd4f Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Aug 2015 18:30:31 +0200 Subject: [PATCH 7/8] Update changelog. --- CHANGELOG.asciidoc | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc index 282f776fc..353f5b25a 100644 --- a/CHANGELOG.asciidoc +++ b/CHANGELOG.asciidoc @@ -32,6 +32,8 @@ Added - New setting `tabs -> show` which supersedes the old `tabs -> hide-*` options and has an additional `switching` option which shows tab while switching them. There's also a new `show-switching` option to configure the timeout. +- New setting `storage -> remember-download-directory` to remember the last + used download directory. Changed ~~~~~~~ @@ -48,7 +50,12 @@ Fixed ~~~~~ - `link_pyqt.py` now should work better on untested distributions. -- Fixed various corner-cases with crashes when reading invalid config values. +- Fixed various corner-cases with crashes when reading invalid config values + and the history file. +- Fixed various corner-cases when setting text via an external editor. +- Fixed potential crash when hinting a text field. +- Fixed entering of insert mode when certain disabled text fields were clicked. +- Fixed a crash when using `:set` with `-p` and `!` (invert value) Removed ~~~~~~~ From c6c14e967d59abe9111f2cd6c0f1e0894707331a Mon Sep 17 00:00:00 2001 From: Florian Bruhin Date: Wed, 5 Aug 2015 23:33:58 +0200 Subject: [PATCH 8/8] Change Position conftypes to top/bottom/left/right. --- doc/help/settings.asciidoc | 16 ++++++++-------- qutebrowser/config/config.py | 16 ++++++++++++++++ qutebrowser/config/configdata.py | 4 ++-- qutebrowser/config/configtypes.py | 12 ++++++------ qutebrowser/mainwindow/mainwindow.py | 4 ++-- 5 files changed, 34 insertions(+), 18 deletions(-) diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc index cd11988b8..3cb7f400f 100644 --- a/doc/help/settings.asciidoc +++ b/doc/help/settings.asciidoc @@ -473,10 +473,10 @@ Where to show the downloaded files. Valid values: - * +north+ - * +south+ + * +top+ + * +bottom+ -Default: +pass:[north]+ +Default: +pass:[top]+ [[ui-message-timeout]] === message-timeout @@ -1020,12 +1020,12 @@ The position of the tab bar. Valid values: - * +north+ - * +south+ - * +east+ - * +west+ + * +top+ + * +bottom+ + * +left+ + * +right+ -Default: +pass:[north]+ +Default: +pass:[top]+ [[tabs-show-favicons]] === show-favicons diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index e88ec93fd..46eb45a7b 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -271,6 +271,20 @@ def _get_value_transformer(old, new): return transformer +def _transform_position(val): + """Transformer for position values.""" + mapping = { + 'north': 'top', + 'south': 'bottom', + 'west': 'left', + 'east': 'right', + } + try: + return mapping[val] + except KeyError: + return val + + class ConfigManager(QObject): """Configuration manager for qutebrowser. @@ -334,6 +348,8 @@ class ConfigManager(QObject): CHANGED_OPTIONS = { ('content', 'cookies-accept'): _get_value_transformer('default', 'no-3rdparty'), + ('tabbar', 'position'): _transform_position, + ('ui', 'downloads-position'): _transform_position, } changed = pyqtSignal(str, str) diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 75fd1b6ad..c20781d99 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -240,7 +240,7 @@ def data(readonly=False): "The default zoom level."), ('downloads-position', - SettingValue(typ.VerticalPosition(), 'north'), + SettingValue(typ.VerticalPosition(), 'top'), "Where to show the downloaded files."), ('message-timeout', @@ -501,7 +501,7 @@ def data(readonly=False): "On which mouse button to close tabs."), ('position', - SettingValue(typ.Position(), 'north'), + SettingValue(typ.Position(), 'top'), "The position of the tab bar."), ('show-favicons', diff --git a/qutebrowser/config/configtypes.py b/qutebrowser/config/configtypes.py index e6e2ab0af..dd4f19d79 100644 --- a/qutebrowser/config/configtypes.py +++ b/qutebrowser/config/configtypes.py @@ -1243,13 +1243,13 @@ class Position(MappingType): """The position of the tab bar.""" - valid_values = ValidValues('north', 'south', 'east', 'west') + valid_values = ValidValues('top', 'bottom', 'left', 'right') MAPPING = { - 'north': QTabWidget.North, - 'south': QTabWidget.South, - 'west': QTabWidget.West, - 'east': QTabWidget.East, + 'top': QTabWidget.North, + 'bottom': QTabWidget.South, + 'left': QTabWidget.West, + 'right': QTabWidget.East, } @@ -1257,7 +1257,7 @@ class VerticalPosition(BaseType): """The position of the download bar.""" - valid_values = ValidValues('north', 'south') + valid_values = ValidValues('top', 'bottom') class UrlList(List): diff --git a/qutebrowser/mainwindow/mainwindow.py b/qutebrowser/mainwindow/mainwindow.py index d9e58802a..51f73a868 100644 --- a/qutebrowser/mainwindow/mainwindow.py +++ b/qutebrowser/mainwindow/mainwindow.py @@ -198,10 +198,10 @@ class MainWindow(QWidget): self._vbox.removeWidget(self._downloadview) self._vbox.removeWidget(self.status) position = config.get('ui', 'downloads-position') - if position == 'north': + if position == 'top': self._vbox.addWidget(self._downloadview) self._vbox.addWidget(self.tabbed_browser) - elif position == 'south': + elif position == 'bottom': self._vbox.addWidget(self.tabbed_browser) self._vbox.addWidget(self._downloadview) else: