summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--qutebrowser/app.py6
-rw-r--r--qutebrowser/browser/webengine/webenginesettings.py242
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py203
-rw-r--r--qutebrowser/utils/qtutils.py14
4 files changed, 259 insertions, 206 deletions
diff --git a/qutebrowser/app.py b/qutebrowser/app.py
index db7eea608..69f181483 100644
--- a/qutebrowser/app.py
+++ b/qutebrowser/app.py
@@ -490,6 +490,9 @@ def _init_modules(*, args):
log.init.debug("Initializing command history...")
cmdhistory.init()
+ log.init.debug("Initializing Greasemonkey...")
+ greasemonkey.init()
+
log.init.debug("Initializing websettings...")
websettings.init(args)
quitter.instance.shutting_down.connect(websettings.shutdown)
@@ -517,9 +520,6 @@ def _init_modules(*, args):
log.init.debug("Initializing downloads...")
qtnetworkdownloads.init()
- log.init.debug("Initializing Greasemonkey...")
- greasemonkey.init()
-
log.init.debug("Misc initialization...")
macros.init()
windowundo.init()
diff --git a/qutebrowser/browser/webengine/webenginesettings.py b/qutebrowser/browser/webengine/webenginesettings.py
index 8a8c4766f..e6c4bae98 100644
--- a/qutebrowser/browser/webengine/webenginesettings.py
+++ b/qutebrowser/browser/webengine/webenginesettings.py
@@ -27,20 +27,25 @@ Module attributes:
import os
import operator
import pathlib
+import functools
+import dataclasses
from typing import cast, Any, List, Optional, Tuple, Union, TYPE_CHECKING
from qutebrowser.qt import machinery
from qutebrowser.qt.gui import QFont
from qutebrowser.qt.widgets import QApplication
-from qutebrowser.qt.webenginecore import QWebEngineSettings, QWebEngineProfile
+from qutebrowser.qt.webenginecore import (
+ QWebEngineSettings, QWebEngineProfile, QWebEngineScript,
+)
-from qutebrowser.browser import history
+from qutebrowser.browser import history, shared, greasemonkey
from qutebrowser.browser.webengine import (spell, webenginequtescheme, cookies,
webenginedownloads, notification)
from qutebrowser.config import config, websettings
from qutebrowser.config.websettings import AttributeInfo as Attr
from qutebrowser.utils import (standarddir, qtutils, message, log,
- urlmatch, usertypes, objreg, version)
+ urlmatch, usertypes, objreg, version,
+ javascript, resources, utils)
if TYPE_CHECKING:
from qutebrowser.browser.webengine import interceptor
@@ -359,6 +364,235 @@ def init_user_agent():
_init_user_agent_str(QWebEngineProfile.defaultProfile().httpUserAgent())
+# TODO: add to javascript module?
+def _script_factory(name, js_code, *,
+ world=QWebEngineScript.ScriptWorldId.ApplicationWorld,
+ injection_point=QWebEngineScript.InjectionPoint.DocumentCreation,
+ subframes=False):
+ """Inject the given script to run early on a page load."""
+ script = QWebEngineScript()
+ script.setInjectionPoint(injection_point)
+ script.setSourceCode(js_code)
+ script.setWorldId(world)
+ script.setRunsOnSubFrames(subframes)
+ script.setName(f'_qute_{name}')
+ return script
+
+
+def _remove_js(scripts, name):
+ """Remove an early QWebEngineScript."""
+ if machinery.IS_QT6:
+ for script in scripts.find(f'_qute_{name}'):
+ scripts.remove(script)
+ else: # Qt 5
+ script = scripts.findScript(f'_qute_{name}')
+ if not script.isNull():
+ scripts.remove(script)
+
+
+# TODO: unrelated rambling
+# Hmm, change_filter can be told it is being passed a function (unbound
+# method) or method (method on an instantiated object). Here I'm telling it
+# these are object methods, although they aren't, just because the only
+# difference between those modes is that an argument is passed through for the
+# object methods. Called "self" in the wrapper it doesn't have to be.
+# Probably the change_filter decorator could be changed to support passing
+# trough variable arguments and get rid of that split?
+# Also it would be nice to have a decorator that did the change filtering and
+# handled connecting a signal, and passed the new value into the function.
+@config.change_filter('scrolling.bar')
+@config.change_filter('content.user_stylesheets')
+def _stylesheet_option_changed(profile):
+ _inject_stylesheet(profile.scripts())
+
+
+def _inject_stylesheet(scripts):
+ """Initialize custom stylesheets.
+
+ Stylesheet CSS is also overriden in individual tabs when config is updated
+ and when find operations are started and ended.
+
+ Partially inspired by QupZilla:
+ https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
+ """
+ _remove_js(scripts, 'stylesheet')
+ css = shared.get_user_stylesheet()
+ js_code = javascript.wrap_global(
+ 'stylesheet',
+ resources.read_file('javascript/stylesheet.js'),
+ javascript.assemble('stylesheet', 'set_css', css),
+ )
+ scripts.insert(_script_factory('stylesheet', js_code, subframes=True))
+
+
+def _remove_all_greasemonkey_scripts(profile_scripts):
+ for script in profile_scripts.toList():
+ if script.name().startswith("GM-"):
+ log.greasemonkey.debug('Removing script: {}'
+ .format(script.name()))
+ removed = profile_scripts.remove(script)
+ assert removed, script.name()
+
+
+def _inject_all_greasemonkey_scripts(profile):
+ scripts = greasemonkey.gm_manager.all_scripts()
+ _inject_greasemonkey_scripts(profile, scripts)
+
+
+def _inject_greasemonkey_scripts(profile, scripts):
+ """Register user JavaScript files with the current tab.
+
+ Args:
+ scripts: A list of GreasemonkeyScripts.
+ """
+ profile_scripts = profile.scripts()
+ # Remove and re-add all scripts every time to account for scripts
+ # that have been disabled.
+ _remove_all_greasemonkey_scripts(profile_scripts)
+
+ seen_names = set()
+ for script in scripts:
+ while script.full_name() in seen_names:
+ script.dedup_suffix += 1
+ seen_names.add(script.full_name())
+
+ # TODO: move to use _script_factory to shorten the method?
+ new_script = QWebEngineScript()
+
+ try:
+ world = int(script.jsworld)
+ if not 0 <= world <= qtutils.MAX_WORLD_ID:
+ log.greasemonkey.error(
+ f"script {script.name} has invalid value for '@qute-js-world'"
+ f": {script.jsworld}, should be between 0 and "
+ f"{qtutils.MAX_WORLD_ID}")
+ continue
+ except ValueError:
+ try:
+ world = qtutils.JS_WORLD_MAP[usertypes.JsWorld[script.jsworld.lower()]]
+ except KeyError:
+ log.greasemonkey.error(
+ f"script {script.name} has invalid value for '@qute-js-world'"
+ f": {script.jsworld}")
+ continue
+ new_script.setWorldId(world)
+
+ # Corresponds to "@run-at document-end" which is the default according to
+ # https://wiki.greasespot.net/Metadata_Block#.40run-at - however,
+ # QtWebEngine uses QWebEngineScript.InjectionPoint.Deferred (@run-at document-idle) as
+ # default.
+ #
+ # NOTE that this needs to be done before setSourceCode, so that
+ # QtWebEngine's parsing of GreaseMonkey tags will override it if there is a
+ # @run-at comment.
+ new_script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
+
+ new_script.setSourceCode(script.code())
+ new_script.setName(script.full_name())
+ new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
+
+ if script.needs_document_end_workaround():
+ log.greasemonkey.debug(
+ f"Forcing @run-at document-end for {script.name}")
+ new_script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
+
+ log.greasemonkey.debug(f'adding script: {new_script.name()}')
+ profile_scripts.insert(new_script)
+
+
+@dataclasses.dataclass
+class _Quirk:
+
+ filename: str
+ injection_point: QWebEngineScript.InjectionPoint = (
+ QWebEngineScript.InjectionPoint.DocumentCreation)
+ world: QWebEngineScript.ScriptWorldId = QWebEngineScript.ScriptWorldId.MainWorld
+ predicate: bool = True
+ name: Optional[str] = None
+
+ def __post_init__(self):
+ if self.name is None:
+ self.name = f"js-{self.filename.replace('_', '-')}"
+
+
+def _get_quirks():
+ """Get a list of all available JS quirks."""
+ versions = version.qtwebengine_versions()
+ return [
+ # FIXME:qt6 Double check which of those are still required
+ _Quirk(
+ 'whatsapp_web',
+ injection_point=QWebEngineScript.InjectionPoint.DocumentReady,
+ world=QWebEngineScript.ScriptWorldId.ApplicationWorld,
+ ),
+ _Quirk('discord'),
+ _Quirk(
+ 'googledocs',
+ # will be an UA quirk once we set the JS UA as well
+ name='ua-googledocs',
+ ),
+
+ _Quirk(
+ 'string_replaceall',
+ predicate=versions.webengine < utils.VersionNumber(5, 15, 3),
+ ),
+ _Quirk(
+ 'array_at',
+ predicate=versions.webengine < utils.VersionNumber(6, 3),
+ ),
+ ]
+
+
+def _inject_site_specific_quirks(scripts):
+ """Add site-specific quirk scripts."""
+ if not config.val.content.site_specific_quirks.enabled:
+ return
+
+ for quirk in _get_quirks():
+ if not quirk.predicate:
+ continue
+ src = resources.read_file(f'javascript/quirks/{quirk.filename}.user.js')
+ if quirk.name not in config.val.content.site_specific_quirks.skip:
+ scripts.insert(_script_factory(
+ f'quirk_{quirk.filename}',
+ src,
+ world=quirk.world,
+ injection_point=quirk.injection_point,
+ ))
+
+
+def _init_scripts_for_profile(profile: QWebEngineProfile) -> None:
+ scripts = profile.scripts()
+
+ # Early global scripts
+ js_code = javascript.wrap_global(
+ 'scripts',
+ resources.read_file('javascript/scroll.js'),
+ resources.read_file('javascript/webelem.js'),
+ resources.read_file('javascript/caret.js'),
+ )
+ # FIXME:qtwebengine what about subframes=True?
+ scripts.insert(_script_factory('js', js_code, subframes=True))
+
+ _inject_stylesheet(scripts)
+ config.instance.changed.connect(
+ functools.partial(
+ _stylesheet_option_changed,
+ profile,
+ )
+ )
+
+ greasemonkey.gm_manager.scripts_reloaded.connect(
+ functools.partial(
+ _inject_all_greasemonkey_scripts,
+ profile,
+ )
+ )
+ _inject_all_greasemonkey_scripts(profile)
+
+ _inject_site_specific_quirks(scripts)
+
+
def _init_profile(profile: QWebEngineProfile) -> None:
"""Initialize a new QWebEngineProfile.
@@ -384,6 +618,8 @@ def _init_profile(profile: QWebEngineProfile) -> None:
_global_settings.init_settings()
+ _init_scripts_for_profile(profile)
+
def _init_default_profile():
"""Init the default QWebEngineProfile."""
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index 6f0ea82f3..23933cf7f 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -24,7 +24,7 @@ import functools
import dataclasses
import re
import html as html_utils
-from typing import cast, Union, Optional
+from typing import cast, Union
from qutebrowser.qt.core import (pyqtSignal, pyqtSlot, Qt, QPoint, QPointF, QTimer, QUrl,
QObject)
@@ -33,13 +33,13 @@ from qutebrowser.qt.webenginewidgets import QWebEngineView
from qutebrowser.qt.webenginecore import QWebEnginePage, QWebEngineScript, QWebEngineHistory
from qutebrowser.config import config
-from qutebrowser.browser import browsertab, eventfilter, shared, webelem, greasemonkey
+from qutebrowser.browser import browsertab, eventfilter, shared, webelem
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
webenginesettings, certificateerror,
webengineinspector)
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
- resources, message, jinja, debug, version)
+ message, jinja, debug, version)
from qutebrowser.qt import sip, machinery
from qutebrowser.misc import objects, miscwidgets
@@ -1001,21 +1001,6 @@ class _WebEnginePermissions(QObject):
blocking=True)
-@dataclasses.dataclass
-class _Quirk:
-
- filename: str
- injection_point: QWebEngineScript.InjectionPoint = (
- QWebEngineScript.InjectionPoint.DocumentCreation)
- world: QWebEngineScript.ScriptWorldId = QWebEngineScript.ScriptWorldId.MainWorld
- predicate: bool = True
- name: Optional[str] = None
-
- def __post_init__(self):
- if self.name is None:
- self.name = f"js-{self.filename.replace('_', '-')}"
-
-
class _WebEngineScripts(QObject):
_widget: webview.WebEngineView
@@ -1024,7 +1009,6 @@ class _WebEngineScripts(QObject):
super().__init__(parent)
self._tab = tab
self._widget = cast(webview.WebEngineView, None)
- self._greasemonkey = greasemonkey.gm_manager
def connect_signals(self):
"""Connect signals to our private slots."""
@@ -1037,7 +1021,6 @@ class _WebEngineScripts(QObject):
@pyqtSlot(str)
def _on_config_changed(self, option):
if option in ['scrolling.bar', 'content.user_stylesheets']:
- self._init_stylesheet()
self._update_stylesheet()
@pyqtSlot(bool)
@@ -1047,185 +1030,6 @@ class _WebEngineScripts(QObject):
code = javascript.assemble('stylesheet', 'set_css', css)
self._tab.run_js_async(code)
- def _inject_js(self, name, js_code, *,
- world=QWebEngineScript.ScriptWorldId.ApplicationWorld,
- injection_point=QWebEngineScript.InjectionPoint.DocumentCreation,
- subframes=False):
- """Inject the given script to run early on a page load."""
- script = QWebEngineScript()
- script.setInjectionPoint(injection_point)
- script.setSourceCode(js_code)
- script.setWorldId(world)
- script.setRunsOnSubFrames(subframes)
- script.setName(f'_qute_{name}')
- self._widget.page().scripts().insert(script)
-
- def _remove_js(self, name):
- """Remove an early QWebEngineScript."""
- scripts = self._widget.page().scripts()
- if machinery.IS_QT6:
- for script in scripts.find(f'_qute_{name}'):
- scripts.remove(script)
- else: # Qt 5
- script = scripts.findScript(f'_qute_{name}')
- if not script.isNull():
- scripts.remove(script)
-
- def init(self):
- """Initialize global qutebrowser JavaScript."""
- js_code = javascript.wrap_global(
- 'scripts',
- resources.read_file('javascript/scroll.js'),
- resources.read_file('javascript/webelem.js'),
- resources.read_file('javascript/caret.js'),
- )
- # FIXME:qtwebengine what about subframes=True?
- self._inject_js('js', js_code, subframes=True)
- self._init_stylesheet()
-
- self._greasemonkey.scripts_reloaded.connect(
- self._inject_all_greasemonkey_scripts)
- self._inject_all_greasemonkey_scripts()
- self._inject_site_specific_quirks()
-
- def _init_stylesheet(self):
- """Initialize custom stylesheets.
-
- Partially inspired by QupZilla:
- https://github.com/QupZilla/qupzilla/blob/v2.0/src/lib/app/mainapplication.cpp#L1063-L1101
- """
- self._remove_js('stylesheet')
- css = shared.get_user_stylesheet()
- js_code = javascript.wrap_global(
- 'stylesheet',
- resources.read_file('javascript/stylesheet.js'),
- javascript.assemble('stylesheet', 'set_css', css),
- )
- self._inject_js('stylesheet', js_code, subframes=True)
-
- @pyqtSlot()
- def _inject_all_greasemonkey_scripts(self):
- scripts = self._greasemonkey.all_scripts()
- self._inject_greasemonkey_scripts(scripts)
-
- def _remove_all_greasemonkey_scripts(self):
- page_scripts = self._widget.page().scripts()
- for script in page_scripts.toList():
- if script.name().startswith("GM-"):
- log.greasemonkey.debug('Removing script: {}'
- .format(script.name()))
- removed = page_scripts.remove(script)
- assert removed, script.name()
-
- def _inject_greasemonkey_scripts(self, scripts):
- """Register user JavaScript files with the current tab.
-
- Args:
- scripts: A list of GreasemonkeyScripts.
- """
- if sip.isdeleted(self._widget):
- return
-
- # Since we are inserting scripts into a per-tab collection,
- # rather than just injecting scripts on page load, we need to
- # make sure we replace existing scripts, not just add new ones.
- # While, taking care not to remove any other scripts that might
- # have been added elsewhere, like the one for stylesheets.
- page_scripts = self._widget.page().scripts()
- self._remove_all_greasemonkey_scripts()
-
- seen_names = set()
- for script in scripts:
- while script.full_name() in seen_names:
- script.dedup_suffix += 1
- seen_names.add(script.full_name())
-
- new_script = QWebEngineScript()
-
- try:
- world = int(script.jsworld)
- if not 0 <= world <= qtutils.MAX_WORLD_ID:
- log.greasemonkey.error(
- f"script {script.name} has invalid value for '@qute-js-world'"
- f": {script.jsworld}, should be between 0 and "
- f"{qtutils.MAX_WORLD_ID}")
- continue
- except ValueError:
- try:
- world = _JS_WORLD_MAP[usertypes.JsWorld[script.jsworld.lower()]]
- except KeyError:
- log.greasemonkey.error(
- f"script {script.name} has invalid value for '@qute-js-world'"
- f": {script.jsworld}")
- continue
- new_script.setWorldId(world)
-
- # Corresponds to "@run-at document-end" which is the default according to
- # https://wiki.greasespot.net/Metadata_Block#.40run-at - however,
- # QtWebEngine uses QWebEngineScript.InjectionPoint.Deferred (@run-at document-idle) as
- # default.
- #
- # NOTE that this needs to be done before setSourceCode, so that
- # QtWebEngine's parsing of GreaseMonkey tags will override it if there is a
- # @run-at comment.
- new_script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
-
- new_script.setSourceCode(script.code())
- new_script.setName(script.full_name())
- new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
-
- if script.needs_document_end_workaround():
- log.greasemonkey.debug(
- f"Forcing @run-at document-end for {script.name}")
- new_script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
-
- log.greasemonkey.debug(f'adding script: {new_script.name()}')
- page_scripts.insert(new_script)
-
- def _get_quirks(self):
- """Get a list of all available JS quirks."""
- versions = version.qtwebengine_versions()
- return [
- # FIXME:qt6 Double check which of those are still required
- _Quirk(
- 'whatsapp_web',
- injection_point=QWebEngineScript.InjectionPoint.DocumentReady,
- world=QWebEngineScript.ScriptWorldId.ApplicationWorld,
- ),
- _Quirk('discord'),
- _Quirk(
- 'googledocs',
- # will be an UA quirk once we set the JS UA as well
- name='ua-googledocs',
- ),
-
- _Quirk(
- 'string_replaceall',
- predicate=versions.webengine < utils.VersionNumber(5, 15, 3),
- ),
- _Quirk(
- 'array_at',
- predicate=versions.webengine < utils.VersionNumber(6, 3),
- ),
- ]
-
- def _inject_site_specific_quirks(self):
- """Add site-specific quirk scripts."""
- if not config.val.content.site_specific_quirks.enabled:
- return
-
- for quirk in self._get_quirks():
- if not quirk.predicate:
- continue
- src = resources.read_file(f'javascript/quirks/{quirk.filename}.user.js')
- if quirk.name not in config.val.content.site_specific_quirks.skip:
- self._inject_js(
- f'quirk_{quirk.filename}',
- src,
- world=quirk.world,
- injection_point=quirk.injection_point,
- )
-
class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
@@ -1301,7 +1105,6 @@ class WebEngineTab(browsertab.AbstractTab):
self.backend = usertypes.Backend.QtWebEngine
self._child_event_filter = None
self._saved_zoom = None
- self._scripts.init()
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-65223
self._needs_qtbug65223_workaround = (
version.qtwebengine_versions().webengine < utils.VersionNumber(5, 15, 5))
diff --git a/qutebrowser/utils/qtutils.py b/qutebrowser/utils/qtutils.py
index 0ecc4cba6..d2cd4bb49 100644
--- a/qutebrowser/utils/qtutils.py
+++ b/qutebrowser/utils/qtutils.py
@@ -25,6 +25,7 @@ Module attributes:
MINVALS: A dictionary of C/Qt types (as string) mapped to their minimum
value.
MAX_WORLD_ID: The highest world ID allowed by QtWebEngine.
+ JS_WORLD_MAP: Mapping form usertypes.JsWorld to QWebEngineScript.ScriptWorldId
"""
@@ -41,6 +42,7 @@ from qutebrowser.qt.core import (qVersion, QEventLoop, QDataStream, QByteArray,
QIODevice, QFileDevice, QSaveFile, QT_VERSION_STR,
PYQT_VERSION_STR, QObject, QUrl, QLibraryInfo)
from qutebrowser.qt.gui import QColor
+from qutebrowser.qt.webenginecore import QWebEngineScript
try:
from qutebrowser.qt.webkit import qWebKitVersion
except ImportError: # pragma: no cover
@@ -117,6 +119,18 @@ def version_check(version: str,
MAX_WORLD_ID = 256
+# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
+if not QWebEngineScript:
+ JS_WORLD_MAP = {}
+else:
+ JS_WORLD_MAP = {
+ usertypes.JsWorld.main: QWebEngineScript.ScriptWorldId.MainWorld,
+ usertypes.JsWorld.application: QWebEngineScript.ScriptWorldId.ApplicationWorld,
+ usertypes.JsWorld.user: QWebEngineScript.ScriptWorldId.UserWorld,
+ usertypes.JsWorld.jseval: QWebEngineScript.ScriptWorldId.UserWorld + 1,
+ }
+
+
def is_new_qtwebkit() -> bool:
"""Check if the given version is a new QtWebKit."""
assert qWebKitVersion is not None