Merge remote-tracking branch 'origin/pr/3793'

This commit is contained in:
Florian Bruhin 2018-06-09 22:59:52 +02:00
commit 7fdeeb25b7
7 changed files with 146 additions and 14 deletions

View File

@ -30,6 +30,7 @@ markers =
qtbug60673: Tests which are broken if the conversion from orange selection to real selection is flaky
fake_os: Fake utils.is_* to a fake operating system
unicode_locale: Tests which need an unicode locale to work
qtwebkit6021_skip: Tests which would fail on WebKit version 602.1
qt_log_level_fail = WARNING
qt_log_ignore =
^SpellCheck: .*

View File

@ -31,9 +31,10 @@ import attr
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
javascript, urlmatch)
javascript, urlmatch, version, usertypes)
from qutebrowser.commands import cmdutils
from qutebrowser.browser import downloads
from qutebrowser.misc import objects
def _scripts_dir():
@ -108,13 +109,18 @@ class GreasemonkeyScript:
browser's debugger/inspector will not match up to the line
numbers in the source script directly.
"""
# Don't use Proxy on this webkit version, the support isn't there.
use_proxy = not (
objects.backend == usertypes.Backend.QtWebKit and
version.qWebKitVersion() == '602.1')
template = jinja.js_environment.get_template('greasemonkey_wrapper.js')
return template.render(
scriptName=javascript.string_escape(
"/".join([self.namespace or '', self.name])),
scriptInfo=self._meta_json(),
scriptMeta=javascript.string_escape(self.script_meta),
scriptSource=self._code)
scriptSource=self._code,
use_proxy=use_proxy)
def _meta_json(self):
return json.dumps({

View File

@ -145,9 +145,59 @@
}
};
const unsafeWindow = window;
{% if use_proxy %}
/*
* Try to give userscripts an environment that they expect. Which
* seems to be that the global window object should look the same as
* the page's one and that if a script writes to an attribute of
* window it should be able to access that variable in the global
* scope.
* Use a Proxy to stop scripts from actually changing the global
* window (that's what unsafeWindow is for).
* Use the "with" statement to make the proxy provide what looks
* like global scope.
*
* There are other Proxy functions that we may need to override.
* set, get and has are definitely required.
*/
const unsafeWindow = window;
const qute_gm_window_shadow = {}; // stores local changes to window
const qute_gm_windowProxyHandler = {
get: function(target, prop) {
if (prop in qute_gm_window_shadow)
return qute_gm_window_shadow[prop];
if (prop in target) {
if (typeof target[prop] === 'function' && typeof target[prop].prototype == 'undefined')
// Getting TypeError: Illegal Execution when callers try to execute
// eg addEventListener from here because they were returned
// unbound
return target[prop].bind(target);
return target[prop];
}
},
set: function(target, prop, val) {
return qute_gm_window_shadow[prop] = val;
},
has: function(target, key) {
return key in qute_gm_window_shadow || key in target;
}
};
const qute_gm_window_proxy = new Proxy(
unsafeWindow, qute_gm_windowProxyHandler);
// ====== The actual user script source ====== //
with (qute_gm_window_proxy) {
// We can't return `this` or `qute_gm_window_proxy` from
// `qute_gm_window_proxy.get('window')` because the Proxy implementation
// does typechecking on read-only things. So we have to shadow `window`
// more conventionally here.
const window = qute_gm_window_proxy;
// ====== The actual user script source ====== //
{{ scriptSource }}
// ====== End User Script ====== //
// ====== End User Script ====== //
};
{% else %}
// ====== The actual user script source ====== //
{{ scriptSource }}
// ====== End User Script ====== //
{% endif %}
})();

View File

@ -36,7 +36,7 @@ from helpers import logfail
from helpers.logfail import fail_on_logging
from helpers.messagemock import message_mock
from helpers.fixtures import * # noqa: F403
from qutebrowser.utils import qtutils, standarddir, usertypes, utils
from qutebrowser.utils import qtutils, standarddir, usertypes, utils, version
from qutebrowser.misc import objects
import qutebrowser.app # To register commands
@ -77,6 +77,10 @@ def _apply_platform_markers(config, item):
"https://bugreports.qt.io/browse/QTBUG-60673"),
('unicode_locale', sys.getfilesystemencoding() == 'ascii',
"Skipped because of ASCII locale"),
('qtwebkit6021_skip',
version.qWebKitVersion and
version.qWebKitVersion() == '602.1',
"Broken on WebKit 602.1")
]
for searched_marker, condition, default_reason in markers:
@ -219,8 +223,10 @@ def check_display(request):
@pytest.fixture(autouse=True)
def set_backend(monkeypatch, request):
"""Make sure the backend global is set."""
backend = (usertypes.Backend.QtWebEngine if request.config.webengine
else usertypes.Backend.QtWebKit)
if not request.config.webengine and version.qWebKitVersion:
backend = usertypes.Backend.QtWebKit
else:
backend = usertypes.Backend.QtWebEngine
monkeypatch.setattr(objects, 'backend', backend)

View File

@ -43,10 +43,10 @@ import helpers.stubs as stubsmod
import helpers.utils
from qutebrowser.config import (config, configdata, configtypes, configexc,
configfiles)
from qutebrowser.utils import objreg, standarddir, utils
from qutebrowser.utils import objreg, standarddir, utils, usertypes
from qutebrowser.browser import greasemonkey
from qutebrowser.browser.webkit import cookies
from qutebrowser.misc import savemanager, sql
from qutebrowser.misc import savemanager, sql, objects
from qutebrowser.keyinput import modeman
@ -360,9 +360,10 @@ def qnam(qapp):
@pytest.fixture
def webengineview(qtbot):
def webengineview(qtbot, monkeypatch):
"""Get a QWebEngineView if QtWebEngine is available."""
QtWebEngineWidgets = pytest.importorskip('PyQt5.QtWebEngineWidgets')
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
view = QtWebEngineWidgets.QWebEngineView()
qtbot.add_widget(view)
return view
@ -379,9 +380,10 @@ def webpage(qnam):
@pytest.fixture
def webview(qtbot, webpage):
def webview(qtbot, webpage, monkeypatch):
"""Get a new QWebView object."""
QtWebKitWidgets = pytest.importorskip('PyQt5.QtWebKitWidgets')
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebKit)
view = QtWebKitWidgets.QWebView()
qtbot.add_widget(view)

View File

@ -158,3 +158,70 @@ def test_required_scripts_are_included(download_stub, tmpdir):
# Additionally check that the base script is still being parsed correctly
assert "Script is running." in scripts[0].code()
assert scripts[0].excludes
class TestWindowIsolation:
"""Check that greasemonkey scripts get a shadowed global scope."""
@pytest.fixture
def setup(self):
# pylint: disable=attribute-defined-outside-init
class SetupData:
pass
ret = SetupData()
# Change something in the global scope
ret.setup_script = "window.$ = 'global'"
# Greasemonkey script to report back on its scope.
test_script = greasemonkey.GreasemonkeyScript.parse(
textwrap.dedent("""
// ==UserScript==
// @name scopetest
// ==/UserScript==
// Check the thing the page set is set to the expected type
result.push(window.$);
result.push($);
// Now overwrite it
window.$ = 'shadowed';
// And check everything is how the script would expect it to be
// after just writing to the "global" scope
result.push(window.$);
result.push($);
""")
)
# The compiled source of that scripts with some additional setup
# bookending it.
ret.test_script = "\n".join([
"var result = [];",
test_script.code(),
"""
// Now check that the actual global scope has
// not been overwritten
result.push(window.$);
result.push($);
// And return our findings
result;"""
])
# What we expect the script to report back.
ret.expected = [
"global", "global",
"shadowed", "shadowed",
"global", "global"]
return ret
def test_webengine(self, callback_checker, webengineview, setup):
page = webengineview.page()
page.runJavaScript(setup.setup_script)
page.runJavaScript(setup.test_script, callback_checker.callback)
callback_checker.check(setup.expected)
# The JSCore in 602.1 doesn't fully support Proxy.
@pytest.mark.qtwebkit6021_skip
def test_webkit(self, webview, setup):
elem = webview.page().mainFrame().documentElement()
elem.evaluateJavaScript(setup.setup_script)
result = elem.evaluateJavaScript(setup.test_script)
assert result == setup.expected

View File

@ -26,7 +26,7 @@ import pytest
@pytest.mark.parametrize('js_enabled, expected', [(True, 2.0), (False, None)])
def test_simple_js_webkit(webview, js_enabled, expected):
"""With QtWebKit, evaluateJavaScript works when JS is on."""
# If we get there (because of the webengineview fixture) we can be certain
# If we get there (because of the webview fixture) we can be certain
# QtWebKit is available
from PyQt5.QtWebKit import QWebSettings
webview.settings().setAttribute(QWebSettings.JavascriptEnabled, js_enabled)
@ -37,7 +37,7 @@ def test_simple_js_webkit(webview, js_enabled, expected):
@pytest.mark.parametrize('js_enabled, expected', [(True, 2.0), (False, 2.0)])
def test_element_js_webkit(webview, js_enabled, expected):
"""With QtWebKit, evaluateJavaScript on an element works with JS off."""
# If we get there (because of the webengineview fixture) we can be certain
# If we get there (because of the webview fixture) we can be certain
# QtWebKit is available
from PyQt5.QtWebKit import QWebSettings
webview.settings().setAttribute(QWebSettings.JavascriptEnabled, js_enabled)