Merge branch 'greasemonkey' of https://github.com/toofar/qutebrowser into greasemonkey
This commit is contained in:
commit
3cd2910fa2
@ -64,7 +64,7 @@ from qutebrowser.completion.models import miscmodels
|
|||||||
from qutebrowser.commands import cmdutils, runners, cmdexc
|
from qutebrowser.commands import cmdutils, runners, cmdexc
|
||||||
from qutebrowser.config import config, websettings, configfiles, configinit
|
from qutebrowser.config import config, websettings, configfiles, configinit
|
||||||
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
|
from qutebrowser.browser import (urlmarks, adblock, history, browsertab,
|
||||||
downloads)
|
downloads, greasemonkey)
|
||||||
from qutebrowser.browser.network import proxy
|
from qutebrowser.browser.network import proxy
|
||||||
from qutebrowser.browser.webkit import cookies, cache
|
from qutebrowser.browser.webkit import cookies, cache
|
||||||
from qutebrowser.browser.webkit.network import networkmanager
|
from qutebrowser.browser.webkit.network import networkmanager
|
||||||
@ -491,6 +491,10 @@ def _init_modules(args, crash_handler):
|
|||||||
diskcache = cache.DiskCache(standarddir.cache(), parent=qApp)
|
diskcache = cache.DiskCache(standarddir.cache(), parent=qApp)
|
||||||
objreg.register('cache', diskcache)
|
objreg.register('cache', diskcache)
|
||||||
|
|
||||||
|
log.init.debug("Initializing Greasemonkey...")
|
||||||
|
gm_manager = greasemonkey.GreasemonkeyManager()
|
||||||
|
objreg.register('greasemonkey', gm_manager)
|
||||||
|
|
||||||
log.init.debug("Misc initialization...")
|
log.init.debug("Misc initialization...")
|
||||||
macros.init()
|
macros.init()
|
||||||
# Init backend-specific stuff
|
# Init backend-specific stuff
|
||||||
|
207
qutebrowser/browser/greasemonkey.py
Normal file
207
qutebrowser/browser/greasemonkey.py
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
|
||||||
|
# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||||
|
#
|
||||||
|
# This file is part of qutebrowser.
|
||||||
|
#
|
||||||
|
# qutebrowser is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# qutebrowser is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Load, parse and make avalaible greasemonkey scripts."""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import fnmatch
|
||||||
|
import functools
|
||||||
|
import glob
|
||||||
|
|
||||||
|
import attr
|
||||||
|
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
|
||||||
|
|
||||||
|
from qutebrowser.utils import log, standarddir, jinja
|
||||||
|
from qutebrowser.commands import cmdutils
|
||||||
|
|
||||||
|
|
||||||
|
def _scripts_dir():
|
||||||
|
"""Get the directory of the scripts."""
|
||||||
|
return os.path.join(standarddir.data(), 'greasemonkey')
|
||||||
|
|
||||||
|
|
||||||
|
class GreasemonkeyScript:
|
||||||
|
"""Container class for userscripts, parses metadata blocks."""
|
||||||
|
|
||||||
|
def __init__(self, properties, code):
|
||||||
|
self._code = code
|
||||||
|
self.includes = []
|
||||||
|
self.excludes = []
|
||||||
|
self.description = None
|
||||||
|
self.name = None
|
||||||
|
self.namespace = None
|
||||||
|
self.run_at = None
|
||||||
|
self.script_meta = None
|
||||||
|
self.runs_on_sub_frames = True
|
||||||
|
for name, value in properties:
|
||||||
|
if name == 'name':
|
||||||
|
self.name = value
|
||||||
|
elif name == 'namespace':
|
||||||
|
self.namespace = value
|
||||||
|
elif name == 'description':
|
||||||
|
self.description = value
|
||||||
|
elif name in ['include', 'match']:
|
||||||
|
self.includes.append(value)
|
||||||
|
elif name in ['exclude', 'exclude_match']:
|
||||||
|
self.excludes.append(value)
|
||||||
|
elif name == 'run-at':
|
||||||
|
self.run_at = value
|
||||||
|
elif name == 'noframes':
|
||||||
|
self.runs_on_sub_frames = False
|
||||||
|
|
||||||
|
HEADER_REGEX = r'// ==UserScript==|\n+// ==/UserScript==\n'
|
||||||
|
PROPS_REGEX = r'// @(?P<prop>[^\s]+)\s*(?P<val>.*)'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse(cls, source):
|
||||||
|
"""GreaseMonkeyScript factory.
|
||||||
|
|
||||||
|
Takes a userscript source and returns a GreaseMonkeyScript.
|
||||||
|
Parses the greasemonkey metadata block, if present, to fill out
|
||||||
|
attributes.
|
||||||
|
"""
|
||||||
|
matches = re.split(cls.HEADER_REGEX, source, maxsplit=2)
|
||||||
|
try:
|
||||||
|
_, props, _code = matches
|
||||||
|
except ValueError:
|
||||||
|
props = ""
|
||||||
|
script = cls(re.findall(cls.PROPS_REGEX, props), source)
|
||||||
|
script.script_meta = props
|
||||||
|
if not props:
|
||||||
|
script.includes = ['*']
|
||||||
|
return script
|
||||||
|
|
||||||
|
def code(self):
|
||||||
|
"""Return the processed javascript code of this script.
|
||||||
|
|
||||||
|
Adorns the source code with GM_* methods for greasemonkey
|
||||||
|
compatibility and wraps it in an IFFE to hide it within a
|
||||||
|
lexical scope. Note that this means line numbers in your
|
||||||
|
browser's debugger/inspector will not match up to the line
|
||||||
|
numbers in the source script directly.
|
||||||
|
"""
|
||||||
|
return jinja.js_environment.get_template(
|
||||||
|
'greasemonkey_wrapper.js').render(
|
||||||
|
scriptName="/".join([self.namespace or '', self.name]),
|
||||||
|
scriptInfo=self._meta_json(),
|
||||||
|
scriptMeta=self.script_meta,
|
||||||
|
scriptSource=self._code)
|
||||||
|
|
||||||
|
def _meta_json(self):
|
||||||
|
return json.dumps({
|
||||||
|
'name': self.name,
|
||||||
|
'description': self.description,
|
||||||
|
'matches': self.includes,
|
||||||
|
'includes': self.includes,
|
||||||
|
'excludes': self.excludes,
|
||||||
|
'run-at': self.run_at,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class MatchingScripts(object):
|
||||||
|
"""All userscripts registered to run on a particular url."""
|
||||||
|
|
||||||
|
url = attr.ib()
|
||||||
|
start = attr.ib(default=attr.Factory(list))
|
||||||
|
end = attr.ib(default=attr.Factory(list))
|
||||||
|
idle = attr.ib(default=attr.Factory(list))
|
||||||
|
|
||||||
|
|
||||||
|
class GreasemonkeyManager(QObject):
|
||||||
|
|
||||||
|
"""Manager of userscripts and a greasemonkey compatible environment.
|
||||||
|
|
||||||
|
Signals:
|
||||||
|
scripts_reloaded: Emitted when scripts are reloaded from disk.
|
||||||
|
Any any cached or already-injected scripts should be
|
||||||
|
considered obselete.
|
||||||
|
"""
|
||||||
|
|
||||||
|
scripts_reloaded = pyqtSignal()
|
||||||
|
# https://wiki.greasespot.net/Include_and_exclude_rules#Greaseable_schemes
|
||||||
|
# Limit the schemes scripts can run on due to unreasonable levels of
|
||||||
|
# exploitability
|
||||||
|
greaseable_schemes = ['http', 'https', 'ftp', 'file']
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.load_scripts()
|
||||||
|
|
||||||
|
@cmdutils.register(name='greasemonkey-reload',
|
||||||
|
instance='greasemonkey')
|
||||||
|
def load_scripts(self):
|
||||||
|
"""Re-Read greasemonkey scripts from disk."""
|
||||||
|
self._run_start = []
|
||||||
|
self._run_end = []
|
||||||
|
self._run_idle = []
|
||||||
|
|
||||||
|
scripts_dir = os.path.abspath(_scripts_dir())
|
||||||
|
log.greasemonkey.debug("Reading scripts from: {}".format(scripts_dir))
|
||||||
|
for script_filename in glob.glob(os.path.join(scripts_dir, '*.js')):
|
||||||
|
if not os.path.isfile(script_filename):
|
||||||
|
continue
|
||||||
|
script_path = os.path.join(scripts_dir, script_filename)
|
||||||
|
with open(script_path, encoding='utf-8') as script_file:
|
||||||
|
script = GreasemonkeyScript.parse(script_file.read())
|
||||||
|
if not script.name:
|
||||||
|
script.name = script_filename
|
||||||
|
|
||||||
|
if script.run_at == 'document-start':
|
||||||
|
self._run_start.append(script)
|
||||||
|
elif script.run_at == 'document-end':
|
||||||
|
self._run_end.append(script)
|
||||||
|
elif script.run_at == 'document-idle':
|
||||||
|
self._run_idle.append(script)
|
||||||
|
else:
|
||||||
|
log.greasemonkey.warning("Script {} has invalid run-at "
|
||||||
|
"defined, defaulting to "
|
||||||
|
"document-end"
|
||||||
|
.format(script_path))
|
||||||
|
# Default as per
|
||||||
|
# https://wiki.greasespot.net/Metadata_Block#.40run-at
|
||||||
|
self._run_end.append(script)
|
||||||
|
log.greasemonkey.debug("Loaded script: {}".format(script.name))
|
||||||
|
self.scripts_reloaded.emit()
|
||||||
|
|
||||||
|
def scripts_for(self, url):
|
||||||
|
"""Fetch scripts that are registered to run for url.
|
||||||
|
|
||||||
|
returns a tuple of lists of scripts meant to run at (document-start,
|
||||||
|
document-end, document-idle)
|
||||||
|
"""
|
||||||
|
if url.scheme() not in self.greaseable_schemes:
|
||||||
|
return MatchingScripts(url, [], [], [])
|
||||||
|
match = functools.partial(fnmatch.fnmatch,
|
||||||
|
url.toString(QUrl.FullyEncoded))
|
||||||
|
tester = (lambda script:
|
||||||
|
any(match(pat) for pat in script.includes) and
|
||||||
|
not any(match(pat) for pat in script.excludes))
|
||||||
|
return MatchingScripts(
|
||||||
|
url,
|
||||||
|
[script for script in self._run_start if tester(script)],
|
||||||
|
[script for script in self._run_end if tester(script)],
|
||||||
|
[script for script in self._run_idle if tester(script)]
|
||||||
|
)
|
||||||
|
|
||||||
|
def all_scripts(self):
|
||||||
|
"""Return all scripts found in the configured script directory."""
|
||||||
|
return self._run_start + self._run_end + self._run_idle
|
@ -244,6 +244,44 @@ def _init_profiles():
|
|||||||
private_profile.setSpellCheckEnabled(True)
|
private_profile.setSpellCheckEnabled(True)
|
||||||
|
|
||||||
|
|
||||||
|
def inject_userscripts():
|
||||||
|
"""Register user javascript files with the global profiles."""
|
||||||
|
# The greasemonkey metadata block support in qtwebengine only starts at 5.8
|
||||||
|
# Otherwise have to handle injecting the scripts into the page at very
|
||||||
|
# early load, probs same place in view as the enableJS check.
|
||||||
|
if not qtutils.version_check('5.8'):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Since we are inserting scripts into profile.scripts they won't
|
||||||
|
# just get replaced by new gm scripts like if we were injecting them
|
||||||
|
# ourselves so we need to remove all gm scripts, while not removing
|
||||||
|
# any other stuff that might have been added. Like the one for
|
||||||
|
# stylsheets.
|
||||||
|
# Could either use a different world for gm scripts, check for gm metadata
|
||||||
|
# values (would mean no non-gm userscripts), or check the code for
|
||||||
|
# _qute_script_id
|
||||||
|
for profile in [default_profile, private_profile]:
|
||||||
|
scripts = profile.scripts()
|
||||||
|
for script in scripts.toList():
|
||||||
|
if script.name().startswith("GM-"):
|
||||||
|
log.greasemonkey.debug('removing script: {}'
|
||||||
|
.format(script.name()))
|
||||||
|
scripts.remove(script)
|
||||||
|
|
||||||
|
for profile in [default_profile, private_profile]:
|
||||||
|
scripts = profile.scripts()
|
||||||
|
greasemonkey = objreg.get('greasemonkey')
|
||||||
|
for script in greasemonkey.all_scripts():
|
||||||
|
new_script = QWebEngineScript()
|
||||||
|
new_script.setWorldId(QWebEngineScript.MainWorld)
|
||||||
|
new_script.setSourceCode(script.code())
|
||||||
|
new_script.setName("GM-{}".format(script.name))
|
||||||
|
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||||
|
log.greasemonkey.debug('adding script: {}'
|
||||||
|
.format(new_script.name()))
|
||||||
|
scripts.insert(new_script)
|
||||||
|
|
||||||
|
|
||||||
def init(args):
|
def init(args):
|
||||||
"""Initialize the global QWebSettings."""
|
"""Initialize the global QWebSettings."""
|
||||||
if args.enable_webengine_inspector:
|
if args.enable_webengine_inspector:
|
||||||
|
@ -69,6 +69,10 @@ def init():
|
|||||||
download_manager.install(webenginesettings.private_profile)
|
download_manager.install(webenginesettings.private_profile)
|
||||||
objreg.register('webengine-download-manager', download_manager)
|
objreg.register('webengine-download-manager', download_manager)
|
||||||
|
|
||||||
|
greasemonkey = objreg.get('greasemonkey')
|
||||||
|
greasemonkey.scripts_reloaded.connect(webenginesettings.inject_userscripts)
|
||||||
|
webenginesettings.inject_userscripts()
|
||||||
|
|
||||||
|
|
||||||
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
|
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
|
||||||
_JS_WORLD_MAP = {
|
_JS_WORLD_MAP = {
|
||||||
|
@ -23,12 +23,14 @@ import functools
|
|||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
|
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
|
||||||
from PyQt5.QtGui import QPalette
|
from PyQt5.QtGui import QPalette
|
||||||
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEnginePage
|
from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage,
|
||||||
|
QWebEngineScript)
|
||||||
|
|
||||||
from qutebrowser.browser import shared
|
from qutebrowser.browser import shared
|
||||||
from qutebrowser.browser.webengine import certificateerror, webenginesettings
|
from qutebrowser.browser.webengine import certificateerror, webenginesettings
|
||||||
from qutebrowser.config import config
|
from qutebrowser.config import config
|
||||||
from qutebrowser.utils import log, debug, usertypes, jinja, urlutils, message
|
from qutebrowser.utils import (log, debug, usertypes, jinja, urlutils, message,
|
||||||
|
objreg, qtutils)
|
||||||
|
|
||||||
|
|
||||||
class WebEngineView(QWebEngineView):
|
class WebEngineView(QWebEngineView):
|
||||||
@ -135,6 +137,7 @@ class WebEnginePage(QWebEnginePage):
|
|||||||
self._theme_color = theme_color
|
self._theme_color = theme_color
|
||||||
self._set_bg_color()
|
self._set_bg_color()
|
||||||
config.instance.changed.connect(self._set_bg_color)
|
config.instance.changed.connect(self._set_bg_color)
|
||||||
|
self.urlChanged.connect(self._inject_userjs)
|
||||||
|
|
||||||
@config.change_filter('colors.webpage.bg')
|
@config.change_filter('colors.webpage.bg')
|
||||||
def _set_bg_color(self):
|
def _set_bg_color(self):
|
||||||
@ -300,3 +303,42 @@ class WebEnginePage(QWebEnginePage):
|
|||||||
message.error(msg)
|
message.error(msg)
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@pyqtSlot('QUrl')
|
||||||
|
def _inject_userjs(self, url):
|
||||||
|
"""Inject userscripts registered for `url` into the current page."""
|
||||||
|
if qtutils.version_check('5.8'):
|
||||||
|
# Handled in webenginetab with the builtin greasemonkey
|
||||||
|
# support.
|
||||||
|
return
|
||||||
|
|
||||||
|
# Using QWebEnginePage.scripts() to hold the user scripts means
|
||||||
|
# we don't have to worry ourselves about where to inject the
|
||||||
|
# page but also means scripts hang around for the tab lifecycle.
|
||||||
|
# So clear them here.
|
||||||
|
scripts = self.scripts()
|
||||||
|
for script in scripts.toList():
|
||||||
|
if script.name().startswith("GM-"):
|
||||||
|
really_removed = scripts.remove(script)
|
||||||
|
log.greasemonkey.debug("Removing ({}) script: {}"
|
||||||
|
.format(really_removed, script.name()))
|
||||||
|
|
||||||
|
def _add_script(script, injection_point):
|
||||||
|
new_script = QWebEngineScript()
|
||||||
|
new_script.setInjectionPoint(injection_point)
|
||||||
|
new_script.setWorldId(QWebEngineScript.MainWorld)
|
||||||
|
new_script.setSourceCode(script.code())
|
||||||
|
new_script.setName("GM-{}".format(script.name))
|
||||||
|
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
|
||||||
|
log.greasemonkey.debug("Adding script: {}"
|
||||||
|
.format(new_script.name()))
|
||||||
|
scripts.insert(new_script)
|
||||||
|
|
||||||
|
greasemonkey = objreg.get('greasemonkey')
|
||||||
|
matching_scripts = greasemonkey.scripts_for(url)
|
||||||
|
for script in matching_scripts.start:
|
||||||
|
_add_script(script, QWebEngineScript.DocumentCreation)
|
||||||
|
for script in matching_scripts.end:
|
||||||
|
_add_script(script, QWebEngineScript.DocumentReady)
|
||||||
|
for script in matching_scripts.idle:
|
||||||
|
_add_script(script, QWebEngineScript.Deferred)
|
||||||
|
@ -86,6 +86,21 @@ class BrowserPage(QWebPage):
|
|||||||
self.on_save_frame_state_requested)
|
self.on_save_frame_state_requested)
|
||||||
self.restoreFrameStateRequested.connect(
|
self.restoreFrameStateRequested.connect(
|
||||||
self.on_restore_frame_state_requested)
|
self.on_restore_frame_state_requested)
|
||||||
|
self.loadFinished.connect(
|
||||||
|
functools.partial(self._inject_userjs, self.mainFrame()))
|
||||||
|
self.frameCreated.connect(self._connect_userjs_signals)
|
||||||
|
|
||||||
|
@pyqtSlot('QWebFrame*')
|
||||||
|
def _connect_userjs_signals(self, frame):
|
||||||
|
"""Connect userjs related signals to `frame`.
|
||||||
|
|
||||||
|
Connect the signals used as triggers for injecting user
|
||||||
|
javascripts into the passed QWebFrame.
|
||||||
|
"""
|
||||||
|
log.greasemonkey.debug("Connecting to frame {} ({})"
|
||||||
|
.format(frame, frame.url().toDisplayString()))
|
||||||
|
frame.loadFinished.connect(
|
||||||
|
functools.partial(self._inject_userjs, frame))
|
||||||
|
|
||||||
def javaScriptPrompt(self, frame, js_msg, default):
|
def javaScriptPrompt(self, frame, js_msg, default):
|
||||||
"""Override javaScriptPrompt to use qutebrowser prompts."""
|
"""Override javaScriptPrompt to use qutebrowser prompts."""
|
||||||
@ -283,6 +298,38 @@ class BrowserPage(QWebPage):
|
|||||||
else:
|
else:
|
||||||
self.error_occurred = False
|
self.error_occurred = False
|
||||||
|
|
||||||
|
def _inject_userjs(self, frame):
|
||||||
|
"""Inject user javascripts into the page.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
frame: The QWebFrame to inject the user scripts into.
|
||||||
|
"""
|
||||||
|
url = frame.url()
|
||||||
|
if url.isEmpty():
|
||||||
|
url = frame.requestedUrl()
|
||||||
|
|
||||||
|
log.greasemonkey.debug("_inject_userjs called for {} ({})"
|
||||||
|
.format(frame, url.toDisplayString()))
|
||||||
|
|
||||||
|
greasemonkey = objreg.get('greasemonkey')
|
||||||
|
scripts = greasemonkey.scripts_for(url)
|
||||||
|
# QtWebKit has trouble providing us with signals representing
|
||||||
|
# page load progress at reasonable times, so we just load all
|
||||||
|
# scripts on the same event.
|
||||||
|
toload = scripts.start + scripts.end + scripts.idle
|
||||||
|
|
||||||
|
if url.isEmpty():
|
||||||
|
# This happens during normal usage like with view source but may
|
||||||
|
# also indicate a bug.
|
||||||
|
log.greasemonkey.debug("Not running scripts for frame with no "
|
||||||
|
"url: {}".format(frame))
|
||||||
|
assert not toload, toload
|
||||||
|
|
||||||
|
for script in toload:
|
||||||
|
if frame is self.mainFrame() or script.runs_on_sub_frames:
|
||||||
|
log.webview.debug('Running GM script: {}'.format(script.name))
|
||||||
|
frame.evaluateJavaScript(script.code())
|
||||||
|
|
||||||
@pyqtSlot('QWebFrame*', 'QWebPage::Feature')
|
@pyqtSlot('QWebFrame*', 'QWebPage::Feature')
|
||||||
def _on_feature_permission_requested(self, frame, feature):
|
def _on_feature_permission_requested(self, frame, feature):
|
||||||
"""Ask the user for approval for geolocation/notifications."""
|
"""Ask the user for approval for geolocation/notifications."""
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
# Upstream Mozilla's code
|
# Upstream Mozilla's code
|
||||||
pac_utils.js
|
pac_utils.js
|
||||||
|
# Actually a jinja template so eslint chokes on the {{}} syntax.
|
||||||
|
greasemonkey_wrapper.js
|
||||||
|
118
qutebrowser/javascript/greasemonkey_wrapper.js
Normal file
118
qutebrowser/javascript/greasemonkey_wrapper.js
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
(function () {
|
||||||
|
const _qute_script_id = "__gm_"+{{ scriptName | tojson }};
|
||||||
|
|
||||||
|
function GM_log(text) {
|
||||||
|
console.log(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
const GM_info = {
|
||||||
|
'script': {{ scriptInfo }},
|
||||||
|
'scriptMetaStr': {{ scriptMeta | tojson }},
|
||||||
|
'scriptWillUpdate': false,
|
||||||
|
'version': "0.0.1",
|
||||||
|
'scriptHandler': 'Tampermonkey' // so scripts don't expect exportFunction
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkKey(key, funcName) {
|
||||||
|
if (typeof key !== "string") {
|
||||||
|
throw new Error(`${funcName} requires the first parameter to be of type string, not '${typeof key}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function GM_setValue(key, value) {
|
||||||
|
checkKey(key, "GM_setValue");
|
||||||
|
if (typeof value !== "string" &&
|
||||||
|
typeof value !== "number" &&
|
||||||
|
typeof value !== "boolean") {
|
||||||
|
throw new Error(`GM_setValue requires the second parameter to be of type string, number or boolean, not '${typeof value}'`);
|
||||||
|
}
|
||||||
|
localStorage.setItem(_qute_script_id + key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GM_getValue(key, default_) {
|
||||||
|
checkKey(key, "GM_getValue");
|
||||||
|
return localStorage.getItem(_qute_script_id + key) || default_;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GM_deleteValue(key) {
|
||||||
|
checkKey(key, "GM_deleteValue");
|
||||||
|
localStorage.removeItem(_qute_script_id + key);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GM_listValues() {
|
||||||
|
let keys = [];
|
||||||
|
for (let i = 0; i < localStorage.length; i++) {
|
||||||
|
if (localStorage.key(i).startsWith(_qute_script_id)) {
|
||||||
|
keys.push(localStorage.key(i).slice(_qute_script_id.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GM_openInTab(url) {
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Almost verbatim copy from Eric
|
||||||
|
function GM_xmlhttpRequest(/* object */ details) {
|
||||||
|
details.method = details.method ? details.method.toUpperCase() : "GET";
|
||||||
|
|
||||||
|
if (!details.url) {
|
||||||
|
throw ("GM_xmlhttpRequest requires an URL.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// build XMLHttpRequest object
|
||||||
|
let oXhr = new XMLHttpRequest();
|
||||||
|
// run it
|
||||||
|
if ("onreadystatechange" in details) {
|
||||||
|
oXhr.onreadystatechange = function () {
|
||||||
|
details.onreadystatechange(oXhr);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if ("onload" in details) {
|
||||||
|
oXhr.onload = function () { details.onload(oXhr) };
|
||||||
|
}
|
||||||
|
if ("onerror" in details) {
|
||||||
|
oXhr.onerror = function () { details.onerror(oXhr) };
|
||||||
|
}
|
||||||
|
|
||||||
|
oXhr.open(details.method, details.url, true);
|
||||||
|
|
||||||
|
if ("headers" in details) {
|
||||||
|
for (let header in details.headers) {
|
||||||
|
oXhr.setRequestHeader(header, details.headers[header]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("data" in details) {
|
||||||
|
oXhr.send(details.data);
|
||||||
|
} else {
|
||||||
|
oXhr.send();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function GM_addStyle(/* String */ styles) {
|
||||||
|
let oStyle = document.createElement("style");
|
||||||
|
oStyle.setAttribute("type", "text/css");
|
||||||
|
oStyle.appendChild(document.createTextNode(styles));
|
||||||
|
|
||||||
|
let head = document.getElementsByTagName("head")[0];
|
||||||
|
if (head === undefined) {
|
||||||
|
document.onreadystatechange = function () {
|
||||||
|
if (document.readyState == "interactive") {
|
||||||
|
document.getElementsByTagName("head")[0].appendChild(oStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
head.appendChild(oStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsafeWindow = window;
|
||||||
|
|
||||||
|
//====== The actual user script source ======//
|
||||||
|
{{ scriptSource }}
|
||||||
|
//====== End User Script ======//
|
||||||
|
})();
|
@ -136,3 +136,4 @@ def render(template, **kwargs):
|
|||||||
|
|
||||||
|
|
||||||
environment = Environment()
|
environment = Environment()
|
||||||
|
js_environment = jinja2.Environment(loader=Loader('javascript'))
|
||||||
|
@ -95,7 +95,8 @@ LOGGER_NAMES = [
|
|||||||
'commands', 'signals', 'downloads',
|
'commands', 'signals', 'downloads',
|
||||||
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
'js', 'qt', 'rfc6266', 'ipc', 'shlexer',
|
||||||
'save', 'message', 'config', 'sessions',
|
'save', 'message', 'config', 'sessions',
|
||||||
'webelem', 'prompt', 'network', 'sql'
|
'webelem', 'prompt', 'network', 'sql',
|
||||||
|
'greasemonkey'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -144,6 +145,7 @@ webelem = logging.getLogger('webelem')
|
|||||||
prompt = logging.getLogger('prompt')
|
prompt = logging.getLogger('prompt')
|
||||||
network = logging.getLogger('network')
|
network = logging.getLogger('network')
|
||||||
sql = logging.getLogger('sql')
|
sql = logging.getLogger('sql')
|
||||||
|
greasemonkey = logging.getLogger('greasemonkey')
|
||||||
|
|
||||||
|
|
||||||
ram_handler = None
|
ram_handler = None
|
||||||
|
@ -123,3 +123,25 @@ Feature: Javascript stuff
|
|||||||
And I wait for "[*/data/javascript/windowsize.html:*] loaded" in the log
|
And I wait for "[*/data/javascript/windowsize.html:*] loaded" in the log
|
||||||
And I run :tab-next
|
And I run :tab-next
|
||||||
Then the window sizes should be the same
|
Then the window sizes should be the same
|
||||||
|
|
||||||
|
Scenario: Have a greasemonkey script run at page start
|
||||||
|
When I have a greasemonkey file saved for document-start with noframes unset
|
||||||
|
And I run :greasemonkey-reload
|
||||||
|
And I open data/hints/iframe.html
|
||||||
|
# This second reload is required in webengine < 5.8 for scripts
|
||||||
|
# registered to run at document-start, some sort of timing issue.
|
||||||
|
And I run :reload
|
||||||
|
Then the javascript message "Script is running on /data/hints/iframe.html" should be logged
|
||||||
|
|
||||||
|
Scenario: Have a greasemonkey script running on frames
|
||||||
|
When I have a greasemonkey file saved for document-end with noframes unset
|
||||||
|
And I run :greasemonkey-reload
|
||||||
|
And I open data/hints/iframe.html
|
||||||
|
Then the javascript message "Script is running on /data/hints/html/wrapped.html" should be logged
|
||||||
|
|
||||||
|
@flaky
|
||||||
|
Scenario: Have a greasemonkey script running on noframes
|
||||||
|
When I have a greasemonkey file saved for document-end with noframes set
|
||||||
|
And I run :greasemonkey-reload
|
||||||
|
And I open data/hints/iframe.html
|
||||||
|
Then the javascript message "Script is running on /data/hints/html/wrapped.html" should not be logged
|
||||||
|
@ -17,6 +17,8 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
|
||||||
import pytest_bdd as bdd
|
import pytest_bdd as bdd
|
||||||
bdd.scenarios('javascript.feature')
|
bdd.scenarios('javascript.feature')
|
||||||
|
|
||||||
@ -29,3 +31,38 @@ def check_window_sizes(quteproc):
|
|||||||
hidden_size = hidden.message.split()[-1]
|
hidden_size = hidden.message.split()[-1]
|
||||||
visible_size = visible.message.split()[-1]
|
visible_size = visible.message.split()[-1]
|
||||||
assert hidden_size == visible_size
|
assert hidden_size == visible_size
|
||||||
|
|
||||||
|
|
||||||
|
test_gm_script = r"""
|
||||||
|
// ==UserScript==
|
||||||
|
// @name Qutebrowser test userscript
|
||||||
|
// @namespace invalid.org
|
||||||
|
// @include http://localhost:*/data/hints/iframe.html
|
||||||
|
// @include http://localhost:*/data/hints/html/wrapped.html
|
||||||
|
// @exclude ???
|
||||||
|
// @run-at {stage}
|
||||||
|
// {frames}
|
||||||
|
// ==/UserScript==
|
||||||
|
console.log("Script is running on " + window.location.pathname);
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@bdd.when(bdd.parsers.parse("I have a greasemonkey file saved for {stage} "
|
||||||
|
"with noframes {frameset}"))
|
||||||
|
def create_greasemonkey_file(quteproc, stage, frameset):
|
||||||
|
script_path = os.path.join(quteproc.basedir, 'data', 'greasemonkey')
|
||||||
|
try:
|
||||||
|
os.mkdir(script_path)
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
file_path = os.path.join(script_path, 'test.user.js')
|
||||||
|
if frameset == "set":
|
||||||
|
frames = "@noframes"
|
||||||
|
elif frameset == "unset":
|
||||||
|
frames = ""
|
||||||
|
else:
|
||||||
|
raise ValueError("noframes can only be set or unset, "
|
||||||
|
"not {}".format(frameset))
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(test_gm_script.format(stage=stage,
|
||||||
|
frames=frames))
|
||||||
|
@ -527,6 +527,7 @@ class QuteProc(testprocess.Process):
|
|||||||
super().before_test()
|
super().before_test()
|
||||||
self.send_cmd(':config-clear')
|
self.send_cmd(':config-clear')
|
||||||
self._init_settings()
|
self._init_settings()
|
||||||
|
self.clear_data()
|
||||||
|
|
||||||
def _init_settings(self):
|
def _init_settings(self):
|
||||||
"""Adjust some qutebrowser settings after starting."""
|
"""Adjust some qutebrowser settings after starting."""
|
||||||
|
108
tests/unit/javascript/test_greasemonkey.py
Normal file
108
tests/unit/javascript/test_greasemonkey.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||||
|
# Copyright 2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||||
|
|
||||||
|
# This file is part of qutebrowser.
|
||||||
|
#
|
||||||
|
# qutebrowser is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 of the License, or
|
||||||
|
# (at your option) any later version.
|
||||||
|
#
|
||||||
|
# qutebrowser is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Tests for qutebrowser.browser.greasemonkey."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from PyQt5.QtCore import QUrl
|
||||||
|
|
||||||
|
from qutebrowser.browser import greasemonkey
|
||||||
|
|
||||||
|
test_gm_script = """
|
||||||
|
// ==UserScript==
|
||||||
|
// @name Qutebrowser test userscript
|
||||||
|
// @namespace invalid.org
|
||||||
|
// @include http://localhost:*/data/title.html
|
||||||
|
// @match http://trolol*
|
||||||
|
// @exclude https://badhost.xxx/*
|
||||||
|
// @run-at document-start
|
||||||
|
// ==/UserScript==
|
||||||
|
console.log("Script is running.");
|
||||||
|
"""
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.usefixtures('data_tmpdir')
|
||||||
|
|
||||||
|
|
||||||
|
def save_script(script_text, filename):
|
||||||
|
script_path = greasemonkey._scripts_dir()
|
||||||
|
try:
|
||||||
|
os.mkdir(script_path)
|
||||||
|
except FileExistsError:
|
||||||
|
pass
|
||||||
|
file_path = os.path.join(script_path, filename)
|
||||||
|
with open(file_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(script_text)
|
||||||
|
|
||||||
|
|
||||||
|
def test_all():
|
||||||
|
"""Test that a script gets read from file, parsed and returned."""
|
||||||
|
save_script(test_gm_script, 'test.user.js')
|
||||||
|
|
||||||
|
gm_manager = greasemonkey.GreasemonkeyManager()
|
||||||
|
assert (gm_manager.all_scripts()[0].name ==
|
||||||
|
"Qutebrowser test userscript")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("url, expected_matches", [
|
||||||
|
# included
|
||||||
|
('http://trololololololo.com/', 1),
|
||||||
|
# neither included nor excluded
|
||||||
|
('http://aaaaaaaaaa.com/', 0),
|
||||||
|
# excluded
|
||||||
|
('https://badhost.xxx/', 0),
|
||||||
|
])
|
||||||
|
def test_get_scripts_by_url(url, expected_matches):
|
||||||
|
"""Check greasemonkey include/exclude rules work."""
|
||||||
|
save_script(test_gm_script, 'test.user.js')
|
||||||
|
gm_manager = greasemonkey.GreasemonkeyManager()
|
||||||
|
|
||||||
|
scripts = gm_manager.scripts_for(QUrl(url))
|
||||||
|
assert (len(scripts.start + scripts.end + scripts.idle) ==
|
||||||
|
expected_matches)
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_metadata(caplog):
|
||||||
|
"""Run on all sites at document-end is the default."""
|
||||||
|
save_script("var nothing = true;\n", 'nothing.user.js')
|
||||||
|
|
||||||
|
with caplog.at_level(logging.WARNING):
|
||||||
|
gm_manager = greasemonkey.GreasemonkeyManager()
|
||||||
|
|
||||||
|
scripts = gm_manager.scripts_for(QUrl('http://notamatch.invalid/'))
|
||||||
|
assert len(scripts.start + scripts.end + scripts.idle) == 1
|
||||||
|
assert len(scripts.end) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_scheme(caplog):
|
||||||
|
"""qute:// isn't in the list of allowed schemes."""
|
||||||
|
save_script("var nothing = true;\n", 'nothing.user.js')
|
||||||
|
|
||||||
|
with caplog.at_level(logging.WARNING):
|
||||||
|
gm_manager = greasemonkey.GreasemonkeyManager()
|
||||||
|
|
||||||
|
scripts = gm_manager.scripts_for(QUrl('qute://settings'))
|
||||||
|
assert len(scripts.start + scripts.end + scripts.idle) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_emits_signal(qtbot):
|
||||||
|
gm_manager = greasemonkey.GreasemonkeyManager()
|
||||||
|
with qtbot.wait_signal(gm_manager.scripts_reloaded):
|
||||||
|
gm_manager.load_scripts()
|
Loading…
Reference in New Issue
Block a user