diff --git a/qutebrowser/browser/greasemonkey.py b/qutebrowser/browser/greasemonkey.py index 7ef1f0ff2..07fa8d581 100644 --- a/qutebrowser/browser/greasemonkey.py +++ b/qutebrowser/browser/greasemonkey.py @@ -31,7 +31,8 @@ import attr from PyQt5.QtCore import pyqtSignal, QObject, QUrl from qutebrowser.utils import (log, standarddir, jinja, objreg, utils, - javascript, urlmatch, version, usertypes) + javascript, urlmatch, version, usertypes, + qtutils) from qutebrowser.api import cmdutils from qutebrowser.browser import downloads from qutebrowser.misc import objects @@ -116,6 +117,40 @@ class GreasemonkeyScript: script.includes = ['*'] return script + def force_document_end(self): + """Check whether to force @run-at document-end. + + This needs to be done on QtWebEngine with Qt 5.12 for known-broken + scripts. + + On Qt 5.12, accessing the DOM isn't possible with "@run-at + document-start". It was documented to be impossible before, but seems + to work fine. + + However, some scripts do DOM access with "@run-at document-start". Fix + those by forcing them to use document-end instead. + """ + if objects.backend != usertypes.Backend.QtWebEngine: + return False + elif not qtutils.version_check('5.12', compiled=False): + return False + + broken_scripts = [ + ('http://userstyles.org', None), + ('https://github.com/ParticleCore', 'Iridium'), + ] + return any(self._matches_id(namespace=namespace, name=name) + for namespace, name in broken_scripts) + + def _matches_id(self, *, namespace, name): + """Check if this script matches the given namespace/name. + + Both namespace and name can be None in order to match any script. + """ + matches_namespace = namespace is None or self.namespace == namespace + matches_name = name is None or self.name == name + return matches_namespace and matches_name + def code(self): """Return the processed JavaScript code of this script. diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 88c283126..41081abb8 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -1038,9 +1038,15 @@ class _WebEngineScripts(QObject): new_script.setSourceCode(script.code()) new_script.setName("GM-{}".format(script.name)) new_script.setRunsOnSubFrames(script.runs_on_sub_frames) + # Override the @run-at value parsed by QWebEngineScript if desired. if injection_point: new_script.setInjectionPoint(injection_point) + elif script.force_document_end(): + log.greasemonkey.debug("Forcing @run-at document-end for {}" + .format(script.name)) + new_script.setInjectionPoint(QWebEngineScript.DocumentReady) + log.greasemonkey.debug('adding script: {}' .format(new_script.name())) page_scripts.insert(new_script) diff --git a/tests/unit/browser/webengine/test_webenginetab.py b/tests/unit/browser/webengine/test_webenginetab.py index 251abb84a..79ac8d37a 100644 --- a/tests/unit/browser/webengine/test_webenginetab.py +++ b/tests/unit/browser/webengine/test_webenginetab.py @@ -25,8 +25,10 @@ import pytest QtWebEngineWidgets = pytest.importorskip("PyQt5.QtWebEngineWidgets") QWebEnginePage = QtWebEngineWidgets.QWebEnginePage QWebEngineScriptCollection = QtWebEngineWidgets.QWebEngineScriptCollection +QWebEngineScript = QtWebEngineWidgets.QWebEngineScript from qutebrowser.browser import greasemonkey +from qutebrowser.utils import usertypes pytestmark = pytest.mark.usefixtures('greasemonkey_manager') @@ -91,3 +93,26 @@ class TestWebengineScripts: collection = webengine_scripts._widget.page().scripts() assert collection.toList()[-1].worldId() == worldid + + def test_greasemonkey_force_document_end(self, monkeypatch, + webengine_scripts): + """Make sure document-end is forced when needed.""" + monkeypatch.setattr(greasemonkey.objects, 'backend', + usertypes.Backend.QtWebEngine) + monkeypatch.setattr(greasemonkey.qtutils, 'version_check', + lambda version, exact=False, compiled=True: + True) + + scripts = [ + greasemonkey.GreasemonkeyScript([ + ('name', 'Iridium'), + ('namespace', 'https://github.com/ParticleCore'), + ('run-at', 'document-start'), + ], None) + ] + + webengine_scripts._inject_greasemonkey_scripts(scripts) + + collection = webengine_scripts._widget.page().scripts() + script = collection.toList()[-1] + assert script.injectionPoint() == QWebEngineScript.DocumentReady diff --git a/tests/unit/javascript/test_greasemonkey.py b/tests/unit/javascript/test_greasemonkey.py index 6c1c35e06..48d2e075c 100644 --- a/tests/unit/javascript/test_greasemonkey.py +++ b/tests/unit/javascript/test_greasemonkey.py @@ -25,6 +25,7 @@ import pytest import py.path # pylint: disable=no-name-in-module from PyQt5.QtCore import QUrl +from qutebrowser.utils import usertypes from qutebrowser.browser import greasemonkey test_gm_script = r""" @@ -165,6 +166,56 @@ def test_utf8_bom(): assert '// ==UserScript==' in script.code().splitlines() +class TestForceDocumentEnd: + + @pytest.fixture + def patch(self, monkeypatch): + def _patch(*, backend, qt_512): + monkeypatch.setattr(greasemonkey.objects, 'backend', backend) + monkeypatch.setattr(greasemonkey.qtutils, 'version_check', + lambda version, exact=False, compiled=True: + qt_512) + return _patch + + def _get_script(self, *, namespace, name): + source = textwrap.dedent(""" + // ==UserScript== + // @namespace {} + // @name {} + // ==/UserScript== + """.format(namespace, name)) + _save_script(source, 'force.user.js') + + gm_manager = greasemonkey.GreasemonkeyManager() + + scripts = gm_manager.all_scripts() + assert len(scripts) == 1 + return scripts[0] + + @pytest.mark.parametrize('backend, qt_512', [ + (usertypes.Backend.QtWebKit, True), + (usertypes.Backend.QtWebEngine, False), + ]) + def test_not_applicable(self, patch, backend, qt_512): + """Test backend/Qt version combinations which don't need a fix.""" + patch(backend=backend, qt_512=qt_512) + script = self._get_script(namespace='https://github.com/ParticleCore', + name='Iridium') + assert not script.force_document_end() + + @pytest.mark.parametrize('namespace, name, force', [ + ('http://userstyles.org', 'foobar', True), + ('https://github.com/ParticleCore', 'Iridium', True), + ('https://github.com/ParticleCore', 'Foo', False), + ('https://example.org', 'Iridium', False), + ]) + def test_matching(self, patch, namespace, name, force): + """Test matching based on namespace/name.""" + patch(backend=usertypes.Backend.QtWebEngine, qt_512=True) + script = self._get_script(namespace=namespace, name=name) + assert script.force_document_end() == force + + def test_required_scripts_are_included(download_stub, tmpdir): test_require_script = textwrap.dedent(""" // ==UserScript==