Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Alexander Cogneau 2015-08-06 16:40:48 +02:00
commit 9a85b66452
11 changed files with 175 additions and 31 deletions

View File

@ -32,6 +32,8 @@ Added
- New setting `tabs -> show` which supersedes the old `tabs -> hide-*` options - New setting `tabs -> show` which supersedes the old `tabs -> hide-*` options
and has an additional `switching` option which shows tab while switching and has an additional `switching` option which shows tab while switching
them. There's also a new `show-switching` option to configure the timeout. 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 Changed
~~~~~~~ ~~~~~~~
@ -48,7 +50,12 @@ Fixed
~~~~~ ~~~~~
- `link_pyqt.py` now should work better on untested distributions. - `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 Removed
~~~~~~~ ~~~~~~~

View File

@ -89,10 +89,10 @@ Requirements
The following software and libraries are required to run qutebrowser: The following software and libraries are required to run qutebrowser:
* http://www.python.org/[Python] 3.4 * 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 * QtWebKit
* http://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.2.0 or newer * 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] * https://pypi.python.org/pypi/setuptools/[pkg_resources/setuptools]
* http://fdik.org/pyPEG/[pyPEG2] * http://fdik.org/pyPEG/[pyPEG2]
* http://jinja.pocoo.org/[jinja2] * http://jinja.pocoo.org/[jinja2]

View File

@ -473,10 +473,10 @@ Where to show the downloaded files.
Valid values: Valid values:
* +north+ * +top+
* +south+ * +bottom+
Default: +pass:[north]+ Default: +pass:[top]+
[[ui-message-timeout]] [[ui-message-timeout]]
=== message-timeout === message-timeout
@ -1020,12 +1020,12 @@ The position of the tab bar.
Valid values: Valid values:
* +north+ * +top+
* +south+ * +bottom+
* +east+ * +left+
* +west+ * +right+
Default: +pass:[north]+ Default: +pass:[top]+
[[tabs-show-favicons]] [[tabs-show-favicons]]
=== show-favicons === show-favicons

View File

@ -307,6 +307,7 @@ def javascript_escape(text):
('"', r'\"'), # (note it won't hurt when we escape the wrong one). ('"', r'\"'), # (note it won't hurt when we escape the wrong one).
('\n', r'\n'), # We also need to escape newlines for some reason. ('\n', r'\n'), # We also need to escape newlines for some reason.
('\r', r'\r'), ('\r', r'\r'),
('\x00', r'\x00'),
) )
for orig, repl in replacements: for orig, repl in replacements:
text = text.replace(orig, repl) text = text.replace(orig, repl)

View File

@ -488,6 +488,8 @@ class Completer(QObject):
"""Delete the current completion item.""" """Delete the current completion item."""
completion = objreg.get('completion', scope='window', completion = objreg.get('completion', scope='window',
window=self._win_id) window=self._win_id)
if not completion.currentIndex().isValid():
raise cmdexc.CommandError("No item selected!")
try: try:
self.model().srcmodel.delete_cur_item(completion) self.model().srcmodel.delete_cur_item(completion)
except NotImplementedError: except NotImplementedError:

View File

@ -271,6 +271,20 @@ def _get_value_transformer(old, new):
return transformer 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): class ConfigManager(QObject):
"""Configuration manager for qutebrowser. """Configuration manager for qutebrowser.
@ -334,6 +348,8 @@ class ConfigManager(QObject):
CHANGED_OPTIONS = { CHANGED_OPTIONS = {
('content', 'cookies-accept'): ('content', 'cookies-accept'):
_get_value_transformer('default', 'no-3rdparty'), _get_value_transformer('default', 'no-3rdparty'),
('tabbar', 'position'): _transform_position,
('ui', 'downloads-position'): _transform_position,
} }
changed = pyqtSignal(str, str) changed = pyqtSignal(str, str)
@ -674,10 +690,11 @@ class ConfigManager(QObject):
else: else:
try: try:
if option.endswith('!') and value is None: 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' layer = 'temp' if temp else 'conf'
if isinstance(val, bool): if isinstance(val, bool):
self.set(layer, section_, option[:-1], str(not val)) self.set(layer, section_, option, str(not val))
else: else:
raise cmdexc.CommandError( raise cmdexc.CommandError(
"set: Attempted inversion of non-boolean value.") "set: Attempted inversion of non-boolean value.")

View File

@ -240,7 +240,7 @@ def data(readonly=False):
"The default zoom level."), "The default zoom level."),
('downloads-position', ('downloads-position',
SettingValue(typ.VerticalPosition(), 'north'), SettingValue(typ.VerticalPosition(), 'top'),
"Where to show the downloaded files."), "Where to show the downloaded files."),
('message-timeout', ('message-timeout',
@ -501,7 +501,7 @@ def data(readonly=False):
"On which mouse button to close tabs."), "On which mouse button to close tabs."),
('position', ('position',
SettingValue(typ.Position(), 'north'), SettingValue(typ.Position(), 'top'),
"The position of the tab bar."), "The position of the tab bar."),
('show-favicons', ('show-favicons',

View File

@ -1243,13 +1243,13 @@ class Position(MappingType):
"""The position of the tab bar.""" """The position of the tab bar."""
valid_values = ValidValues('north', 'south', 'east', 'west') valid_values = ValidValues('top', 'bottom', 'left', 'right')
MAPPING = { MAPPING = {
'north': QTabWidget.North, 'top': QTabWidget.North,
'south': QTabWidget.South, 'bottom': QTabWidget.South,
'west': QTabWidget.West, 'left': QTabWidget.West,
'east': QTabWidget.East, 'right': QTabWidget.East,
} }
@ -1257,7 +1257,7 @@ class VerticalPosition(BaseType):
"""The position of the download bar.""" """The position of the download bar."""
valid_values = ValidValues('north', 'south') valid_values = ValidValues('top', 'bottom')
class UrlList(List): class UrlList(List):

View File

@ -198,10 +198,10 @@ class MainWindow(QWidget):
self._vbox.removeWidget(self._downloadview) self._vbox.removeWidget(self._downloadview)
self._vbox.removeWidget(self.status) self._vbox.removeWidget(self.status)
position = config.get('ui', 'downloads-position') position = config.get('ui', 'downloads-position')
if position == 'north': if position == 'top':
self._vbox.addWidget(self._downloadview) self._vbox.addWidget(self._downloadview)
self._vbox.addWidget(self.tabbed_browser) self._vbox.addWidget(self.tabbed_browser)
elif position == 'south': elif position == 'bottom':
self._vbox.addWidget(self.tabbed_browser) self._vbox.addWidget(self.tabbed_browser)
self._vbox.addWidget(self._downloadview) self._vbox.addWidget(self._downloadview)
else: else:

View File

@ -25,10 +25,12 @@ from unittest import mock
import collections.abc import collections.abc
import operator import operator
import itertools import itertools
import binascii
import os.path
import hypothesis import hypothesis
import hypothesis.strategies import hypothesis.strategies
from PyQt5.QtCore import QRect, QPoint from PyQt5.QtCore import PYQT_VERSION, QRect, QPoint
from PyQt5.QtWebKit import QWebElement from PyQt5.QtWebKit import QWebElement
import pytest import pytest
@ -513,6 +515,11 @@ class TestJavascriptEscape:
"foo'bar": r"foo\'bar", "foo'bar": r"foo\'bar",
'foo"bar': r'foo\"bar', 'foo"bar': r'foo\"bar',
'one\\two\rthree\nfour\'five"six': r'one\\two\rthree\nfour\'five\"six', '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()) @pytest.mark.parametrize('before, after', TESTS.items())
@ -520,21 +527,63 @@ class TestJavascriptEscape:
"""Test javascript escaping with some expected outcomes.""" """Test javascript escaping with some expected outcomes."""
assert webelem.javascript_escape(before) == after 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*.""" """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) escaped = webelem.javascript_escape(text)
webframe.evaluateJavaScript('window.foo = "{}";'.format(escaped)) path = os.path.join(os.path.dirname(__file__),
assert webframe.evaluateJavaScript('window.foo') == text '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) @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.""" """Test javascript escaping with a real QWebPage."""
self._test_escape(text, webframe) self._test_escape(text, qtbot, webframe)
@hypothesis.given(hypothesis.strategies.text()) @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.""" """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: class TestGetChildFrames:

View File

@ -0,0 +1,68 @@
<!--
Helper file for test_javascript_escape() in test_webelem.py.
Since the conversion from QStrings to Python strings is broken in some corner
cases in PyQt < 5.4 we hexlify the string we got in javascript here and test
that in the test.
-->
<html>
<head>
<script type="text/javascript">
//<![CDATA[
/*
* hexlify() and str2rstr_utf8() are based on:
*
* JavaScript MD5 1.0.1
* https://github.com/blueimp/JavaScript-MD5
*
* Copyright 2011, Sebastian Tschan
* https://blueimp.net
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*
* Based on
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321.
* Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
* Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
* Distributed under the BSD License
* See http://pajhome.org.uk/crypt/md5 for more info.
*/
'use strict';
function hexlify(input) {
var hex_tab = '0123456789abcdef';
var output = '';
var x;
var i;
for (i = 0; i < input.length; i += 1) {
x = input.charCodeAt(i);
output += hex_tab.charAt((x >>> 4) & 0x0F) + hex_tab.charAt(x & 0x0F);
}
return output;
}
function encode_utf8(input) {
return unescape(encodeURIComponent(input));
}
function set_text() {
var elems = document.getElementsByTagName("p");
var hexlified = hexlify(encode_utf8("%INPUT%"));
var result = hexlified + "|" + "%INPUT%";
elems[0].innerHTML = result
window.qute_test_result = result;
}
//]]>
</script>
</head>
<body onload="set_text()">
<p>set_text() not called...</p>
</html>