summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLembrun <amadeusk7@free.fr>2021-03-09 22:41:04 +0100
committerLembrun <amadeusk7@free.fr>2021-03-09 22:41:04 +0100
commit9c056f288aefebbb95734fa3dc46a2405862405b (patch)
tree93a866f887638ca9f0a6e7a1977a71ef05591f38
parentb57dfaf663f66de890e984dd814bf86a9c7a08a5 (diff)
parent0a38fff4c675b384289728a4d45f9c1fe1f9d4fc (diff)
downloadqutebrowser-9c056f288aefebbb95734fa3dc46a2405862405b.tar.gz
qutebrowser-9c056f288aefebbb95734fa3dc46a2405862405b.zip
Merge branch 'master' into pathlib-/unit/commands
-rw-r--r--README.asciidoc4
-rw-r--r--doc/changelog.asciidoc16
-rw-r--r--doc/help/settings.asciidoc14
-rw-r--r--doc/img/cheatsheet-big.pngbin779344 -> 781120 bytes
-rw-r--r--doc/img/cheatsheet-small.pngbin30208 -> 30252 bytes
-rw-r--r--misc/cheatsheet.svg14
-rw-r--r--misc/requirements/requirements-flake8.txt2
-rw-r--r--misc/requirements/requirements-mypy.txt12
-rw-r--r--misc/requirements/requirements-pyinstaller.txt2
-rw-r--r--misc/requirements/requirements-pyroma.txt4
-rw-r--r--misc/requirements/requirements-sphinx.txt4
-rw-r--r--misc/requirements/requirements-tests.txt9
-rw-r--r--misc/requirements/requirements-tests.txt-raw4
-rw-r--r--misc/requirements/requirements-tox.txt4
-rw-r--r--misc/userscripts/README.md2
-rwxr-xr-xmisc/userscripts/qute-keepassxc361
-rwxr-xr-xmisc/userscripts/readability3
-rwxr-xr-xmisc/userscripts/readability-js3
-rw-r--r--pytest.ini1
-rw-r--r--qutebrowser/browser/browsertab.py14
-rw-r--r--qutebrowser/browser/commands.py4
-rw-r--r--qutebrowser/browser/inspector.py25
-rw-r--r--qutebrowser/browser/shared.py67
-rw-r--r--qutebrowser/browser/webengine/webenginetab.py68
-rw-r--r--qutebrowser/browser/webkit/webkittab.py6
-rw-r--r--qutebrowser/config/configdata.yml14
-rw-r--r--qutebrowser/keyinput/modeman.py11
-rw-r--r--qutebrowser/mainwindow/tabbedbrowser.py14
-rw-r--r--qutebrowser/misc/guiprocess.py5
-rw-r--r--qutebrowser/qutebrowser.py3
-rw-r--r--requirements.txt8
-rw-r--r--scripts/dev/ci/docker/Dockerfile.j27
-rwxr-xr-xscripts/dev/run_vulture.py1
-rw-r--r--tests/conftest.py6
-rw-r--r--tests/end2end/features/editor.feature49
-rw-r--r--tests/end2end/features/misc.feature12
-rw-r--r--tests/end2end/features/qutescheme.feature2
-rw-r--r--tests/end2end/features/sessions.feature5
-rw-r--r--tests/end2end/features/tabs.feature5
-rw-r--r--tests/end2end/features/test_editor_bdd.py37
-rw-r--r--tests/unit/browser/webengine/test_webenginetab.py94
-rw-r--r--tests/unit/completion/test_models.py11
-rw-r--r--tests/unit/javascript/conftest.py7
-rw-r--r--tests/unit/javascript/test_greasemonkey.py189
-rw-r--r--tests/unit/misc/test_guiprocess.py2
45 files changed, 785 insertions, 340 deletions
diff --git a/README.asciidoc b/README.asciidoc
index 704058bd7..43e6a19e4 100644
--- a/README.asciidoc
+++ b/README.asciidoc
@@ -92,7 +92,6 @@ websites and using it for transmission of sensitive data._
* https://www.riverbankcomputing.com/software/pyqt/intro[PyQt] 5.12.0 or newer
for Python 3
* https://palletsprojects.com/p/jinja/[jinja2]
-* https://pygments.org/[pygments]
* https://github.com/yaml/pyyaml[PyYAML]
On older Python versions (3.6/3.7/3.8), the following backports are also required:
@@ -103,6 +102,9 @@ On older Python versions (3.6/3.7/3.8), the following backports are also require
The following libraries are optional:
* https://pypi.org/project/adblock/[adblock] (for improved adblocking using ABP syntax)
+* https://pygments.org/[pygments] for syntax highlighting with `:view-source`
+ on QtWebKit, or when using `:view-source --pygments` with the (default)
+ QtWebEngine backend.
* On Windows, https://pypi.python.org/pypi/colorama/[colorama] for colored log
output.
* https://importlib-metadata.readthedocs.io/[importlib_resources] on Python 3.7
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index c5636204f..d57698df7 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -27,6 +27,7 @@ Added
- New optional dependency on the `importlib_metadata` project on Python 3.7 and
below. This is only relevant when PyQtWebEngine is installed via pip - thus,
this dependency usually isn't relevant for packagers.
+- New `qute-keepassxc` userscript integrating with the KeePassXC browser API.
Changed
~~~~~~~
@@ -54,6 +55,17 @@ Changed
long line.
- If a command stats with space (e.g. `: open ...`, it's now not saved to
command history anymore (similar to how some shells work).
+- When a tab is pinned, running `:open` will now open a new tab instead of
+ displaying an error.
+- The `fileselect.*.command` settings now support file selectors writing the
+ selected paths to stdout, which is used if no `{}` placeholder is contained in
+ the configured command.
+- The `--debug-flag` argument now understands a new `log-sensitive-keys` value
+ which logs all keypresses (including those in insert/passthrough/prompt/...
+ mode) for debugging.
+- The `readability` and `readability-js` userscripts now add a
+ `qute-readability` CSS class to the page, so that it can be styled easily via
+ a user stylesheet.
Fixed
~~~~~
@@ -81,6 +93,10 @@ Fixed
properly.
- The "try again" button on error pages now works correctly with JavaScript
disabled.
+- If a GreaseMonkey script doesn't have a "@run-at" comment, qutebrowser
+ accidentally treated that as "@run-at document-idle". However, other
+ GreaseMonkey implementations default to "@run-at document-end" instead, which
+ is what qutebrowser now does, too.
[[v2.0.2]]
v2.0.2 (2021-02-04)
diff --git a/doc/help/settings.asciidoc b/doc/help/settings.asciidoc
index 7d0b3469c..392f60c49 100644
--- a/doc/help/settings.asciidoc
+++ b/doc/help/settings.asciidoc
@@ -207,8 +207,8 @@
|<<editor.command,editor.command>>|Editor (and arguments) to use for the `edit-*` commands.
|<<editor.encoding,editor.encoding>>|Encoding to use for the editor.
|<<fileselect.handler,fileselect.handler>>|Handler for selecting file(s) in forms. If `external`, then the commands specified by `fileselect.single_file.command` and `fileselect.multiple_files.command` are used to select one or multiple files respectively.
-|<<fileselect.multiple_files.command,fileselect.multiple_files.command>>|Command (and arguments) to use for selecting multiple files in forms. The command should write the selected file paths to the specified file, separated by newlines.
-|<<fileselect.single_file.command,fileselect.single_file.command>>|Command (and arguments) to use for selecting a single file in forms. The command should write the selected file path to the specified file.
+|<<fileselect.multiple_files.command,fileselect.multiple_files.command>>|Command (and arguments) to use for selecting multiple files in forms. The command should write the selected file paths to the specified file or to stdout, separated by newlines.
+|<<fileselect.single_file.command,fileselect.single_file.command>>|Command (and arguments) to use for selecting a single file in forms. The command should write the selected file path to the specified file or stdout.
|<<fonts.completion.category,fonts.completion.category>>|Font used in the completion categories.
|<<fonts.completion.entry,fonts.completion.entry>>|Font used in the completion widget.
|<<fonts.contextmenu,fonts.contextmenu>>|Font used for the context menu.
@@ -2825,9 +2825,10 @@ Default: +pass:[default]+
[[fileselect.multiple_files.command]]
=== fileselect.multiple_files.command
-Command (and arguments) to use for selecting multiple files in forms. The command should write the selected file paths to the specified file, separated by newlines.
+Command (and arguments) to use for selecting multiple files in forms. The command should write the selected file paths to the specified file or to stdout, separated by newlines.
The following placeholders are defined:
-* `{}`: Filename of the file to be written to.
+* `{}`: Filename of the file to be written to. If not contained in any argument, the
+ standard output of the command is read instead.
Type: <<types,ShellCommand>>
@@ -2840,9 +2841,10 @@ Default:
[[fileselect.single_file.command]]
=== fileselect.single_file.command
-Command (and arguments) to use for selecting a single file in forms. The command should write the selected file path to the specified file.
+Command (and arguments) to use for selecting a single file in forms. The command should write the selected file path to the specified file or stdout.
The following placeholders are defined:
-* `{}`: Filename of the file to be written to.
+* `{}`: Filename of the file to be written to. If not contained in any argument, the
+ standard output of the command is read instead.
Type: <<types,ShellCommand>>
diff --git a/doc/img/cheatsheet-big.png b/doc/img/cheatsheet-big.png
index ecd52c14e..75e2abb89 100644
--- a/doc/img/cheatsheet-big.png
+++ b/doc/img/cheatsheet-big.png
Binary files differ
diff --git a/doc/img/cheatsheet-small.png b/doc/img/cheatsheet-small.png
index 0dc01e8b4..e97d63367 100644
--- a/doc/img/cheatsheet-small.png
+++ b/doc/img/cheatsheet-small.png
Binary files differ
diff --git a/misc/cheatsheet.svg b/misc/cheatsheet.svg
index 7e8a7b381..e908f9496 100644
--- a/misc/cheatsheet.svg
+++ b/misc/cheatsheet.svg
@@ -11,7 +11,7 @@
height="682.66669"
id="svg2"
sodipodi:version="0.32"
- inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07, custom)"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
version="1.0"
sodipodi:docname="cheatsheet.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
@@ -30,16 +30,16 @@
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
- inkscape:zoom="1.7536248"
- inkscape:cx="466.08451"
- inkscape:cy="268.64059"
+ inkscape:zoom="2.48"
+ inkscape:cx="834.18001"
+ inkscape:cy="692.30401"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="1024px"
height="640px"
showgrid="false"
- inkscape:window-width="3822"
- inkscape:window-height="2128"
+ inkscape:window-width="1914"
+ inkscape:window-height="1048"
inkscape:window-x="0"
inkscape:window-y="16"
showguides="true"
@@ -3113,8 +3113,6 @@
style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
id="flowPara3925">ss - set setting (sl: temp)</flowPara><flowPara
style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
- id="flowPara3927" /><flowPara
- style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
id="flowPara3929">sk - bind key</flowPara><flowPara
style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
id="flowPara3931">Ss - show settings</flowPara><flowPara
diff --git a/misc/requirements/requirements-flake8.txt b/misc/requirements/requirements-flake8.txt
index 493fa3cac..c83b57860 100644
--- a/misc/requirements/requirements-flake8.txt
+++ b/misc/requirements/requirements-flake8.txt
@@ -2,7 +2,7 @@
attrs==20.3.0
flake8==3.8.4
-flake8-bugbear==20.11.1
+flake8-bugbear==21.3.1
flake8-builtins==1.5.3
flake8-comprehensions==3.3.1
flake8-copyright==0.2.2
diff --git a/misc/requirements/requirements-mypy.txt b/misc/requirements/requirements-mypy.txt
index 070339ed6..dfa80656b 100644
--- a/misc/requirements/requirements-mypy.txt
+++ b/misc/requirements/requirements-mypy.txt
@@ -1,10 +1,10 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
chardet==4.0.0
-diff-cover==4.2.1
-importlib-metadata==3.7.0
-importlib-resources==5.1.1
-inflect==5.2.0
+diff-cover==4.2.3
+importlib-metadata==3.7.2
+importlib-resources==5.1.2
+inflect==3.0.2
Jinja2==2.11.3
jinja2-pluralize==0.3.0
lxml==4.6.2
@@ -12,8 +12,8 @@ MarkupSafe==1.1.1
mypy==0.812
mypy-extensions==0.4.3
pluggy==0.13.1
-Pygments==2.8.0
+Pygments==2.8.1
PyQt5-stubs==5.15.2.0
typed-ast==1.4.2
typing-extensions==3.7.4.3
-zipp==3.4.0
+zipp==3.4.1
diff --git a/misc/requirements/requirements-pyinstaller.txt b/misc/requirements/requirements-pyinstaller.txt
index 05a59200f..5b7c0137a 100644
--- a/misc/requirements/requirements-pyinstaller.txt
+++ b/misc/requirements/requirements-pyinstaller.txt
@@ -2,4 +2,4 @@
altgraph==0.17
pyinstaller==4.2
-pyinstaller-hooks-contrib==2020.11
+pyinstaller-hooks-contrib==2021.1
diff --git a/misc/requirements/requirements-pyroma.txt b/misc/requirements/requirements-pyroma.txt
index 22a195e66..b64b99e24 100644
--- a/misc/requirements/requirements-pyroma.txt
+++ b/misc/requirements/requirements-pyroma.txt
@@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.16
-Pygments==2.5.2
-pyroma==2.6.1
+Pygments==2.8.1
+pyroma==3.1
diff --git a/misc/requirements/requirements-sphinx.txt b/misc/requirements/requirements-sphinx.txt
index 495b8dcf5..352be342a 100644
--- a/misc/requirements/requirements-sphinx.txt
+++ b/misc/requirements/requirements-sphinx.txt
@@ -10,12 +10,12 @@ imagesize==1.2.0
Jinja2==2.11.3
MarkupSafe==1.1.1
packaging==20.9
-Pygments==2.8.0
+Pygments==2.8.1
pyparsing==2.4.7
pytz==2021.1
requests==2.25.1
snowballstemmer==2.1.0
-Sphinx==3.5.1
+Sphinx==3.5.2
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==1.0.3
diff --git a/misc/requirements/requirements-tests.txt b/misc/requirements/requirements-tests.txt
index c93b5896d..2bfaf91e0 100644
--- a/misc/requirements/requirements-tests.txt
+++ b/misc/requirements/requirements-tests.txt
@@ -8,14 +8,14 @@ chardet==4.0.0
cheroot==8.5.2
click==7.1.2
# colorama==0.4.4
-coverage==5.5 ; python_version!="3.10"
+coverage==5.5
EasyProcess==0.3
execnet==1.8.0
filelock==3.0.12
Flask==1.1.2
glob2==0.7
hunter==3.3.1
-hypothesis==6.3.4
+hypothesis==6.6.0
icdiff==1.9.1
idna==2.10
iniconfig==1.1.1
@@ -33,7 +33,7 @@ pluggy==0.13.1
pprintpp==0.4.0
py==1.10.0
py-cpuinfo==7.0.0
-Pygments==2.8.0
+Pygments==2.8.1
pyparsing==2.4.7
pytest==6.2.2
pytest-bdd==4.0.2
@@ -48,7 +48,7 @@ pytest-repeat==0.9.1
pytest-rerunfailures==9.1.1
pytest-xdist==2.2.1
pytest-xvfb==2.0.0
-PyVirtualDisplay==2.0
+PyVirtualDisplay==2.1
requests==2.25.1
requests-file==1.5.1
six==1.15.0
@@ -59,4 +59,3 @@ toml==0.10.2
urllib3==1.26.3
vulture==2.3
Werkzeug==1.0.1
-coverage==5.4; python_version=="3.10"
diff --git a/misc/requirements/requirements-tests.txt-raw b/misc/requirements/requirements-tests.txt-raw
index 196a80753..ab580ac4b 100644
--- a/misc/requirements/requirements-tests.txt-raw
+++ b/misc/requirements/requirements-tests.txt-raw
@@ -34,7 +34,3 @@ pytest-icdiff
tldextract
#@ ignore: Jinja2, MarkupSafe, colorama
-
-# WORKAROUND for https://github.com/nedbat/coveragepy/issues/1129
-#@ markers: coverage python_version!="3.10"
-#@ add: coverage==5.4; python_version=="3.10"
diff --git a/misc/requirements/requirements-tox.txt b/misc/requirements/requirements-tox.txt
index 1e6382e1e..d44522118 100644
--- a/misc/requirements/requirements-tox.txt
+++ b/misc/requirements/requirements-tox.txt
@@ -8,9 +8,9 @@ pip==21.0.1
pluggy==0.13.1
py==1.10.0
pyparsing==2.4.7
-setuptools==54.0.0
+setuptools==54.1.1
six==1.15.0
toml==0.10.2
-tox==3.22.0
+tox==3.23.0
virtualenv==20.4.2
wheel==0.36.2
diff --git a/misc/userscripts/README.md b/misc/userscripts/README.md
index 938dd776d..395797805 100644
--- a/misc/userscripts/README.md
+++ b/misc/userscripts/README.md
@@ -17,6 +17,8 @@ The following userscripts are included in the current directory.
current website.
- [qute-keepass](./qute-keepass): Insertion of usernames and passwords from keepass
databases using pykeepass.
+- [qute-keepassxc](./qute-keepassxc): Insert credentials from open KeepassXC database
+ using keepassxc-browser protocol.
- [qute-pass](./qute-pass): Insert login information using pass and a
dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...).
- [qute-lastpass](./qute-lastpass): Similar to qute-pass, for Lastpass.
diff --git a/misc/userscripts/qute-keepassxc b/misc/userscripts/qute-keepassxc
new file mode 100755
index 000000000..f0127590b
--- /dev/null
+++ b/misc/userscripts/qute-keepassxc
@@ -0,0 +1,361 @@
+#!/usr/bin/env python3
+
+# Copyright (c) 2018-2021 Markus Blöchl <ususdei@gmail.com>
+#
+# 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 <https://www.gnu.org/licenses/>.
+
+"""
+# Introduction
+
+This is a [qutebrowser][2] [userscript][5] to fill website credentials from a [KeepassXC][1] password database.
+
+
+# Installation
+
+First, you need to enable [KeepassXC-Browser][6] extensions in your KeepassXC config.
+
+
+Second, you must make sure to have a working private-public-key-pair in your [GPG keyring][3].
+
+
+Third, install the python module `pynacl`.
+
+
+Finally, adapt your qutebrowser config.
+You can e.g. add the following lines to your `~/.config/qutebrowser/config.py`
+Remember to replace `ABC1234` with your actual GPG key.
+
+```python
+config.bind('<Alt-Shift-u>', 'spawn --userscript qute-keepassxc --key ABC1234', mode='insert')
+config.bind('pw', 'spawn --userscript qute-keepassxc --key ABC1234', mode='normal')
+```
+
+
+# Usage
+
+If you are on a webpage with a login form, simply activate one of the configured key-bindings.
+
+The first time you run this script, KeepassXC will ask you for authentication like with any other browser extension.
+Just provide a name of your choice and accept the request if nothing looks fishy.
+
+
+# How it works
+
+This script will talk to KeepassXC using the native [KeepassXC-Browser protocol][4].
+
+
+This script needs to store the key used to associate with your KeepassXC instance somewhere.
+Unlike most browser extensions which only use plain local storage, this one attempts to do so in a safe way
+by storing the key in encrypted form using GPG.
+Therefore you need to have a public-key-pair readily set up.
+
+GPG might then ask for your private-key passwort whenever you query the database for login credentials.
+
+
+[1]: https://keepassxc.org/
+[2]: https://qutebrowser.org/
+[3]: https://gnupg.org/
+[4]: https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md
+[5]: https://github.com/qutebrowser/qutebrowser/blob/master/doc/userscripts.asciidoc
+[6]: https://keepassxc.org/docs/keepassxc-browser-migration/
+"""
+
+import sys
+import os
+import socket
+import json
+import base64
+import subprocess
+import argparse
+
+import nacl.utils
+import nacl.public
+
+
+def parse_args():
+ parser = argparse.ArgumentParser(description="Full passwords from KeepassXC")
+ parser.add_argument('url', nargs='?', default=os.environ.get('QUTE_URL'))
+ parser.add_argument('--socket', '-s', default='/run/user/{}/org.keepassxc.KeePassXC.BrowserServer'.format(os.getuid()),
+ help='Path to KeepassXC browser socket')
+ parser.add_argument('--key', '-k', default='alice@example.com',
+ help='GPG key to encrypt KeepassXC auth key with')
+ parser.add_argument('--insecure', action='store_true',
+ help="Do not encrypt auth key")
+ return parser.parse_args()
+
+
+class KeepassError(Exception):
+ def __init__(self, code, desc):
+ self.code = code
+ self.description = desc
+
+ def __str__(self):
+ return f"KeepassXC Error [{self.code}]: {self.description}"
+
+
+class KeepassXC:
+ """ Wrapper around the KeepassXC socket API """
+ def __init__(self, id=None, *, key, socket_path):
+ self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ self.id = id
+ self.socket_path = socket_path
+ self.client_key = nacl.public.PrivateKey.generate()
+ self.id_key = nacl.public.PrivateKey.from_seed(key)
+ self.cryptobox = None
+
+ def connect(self):
+ if not os.path.exists(self.socket_path):
+ raise KeepassError(-1, "KeepassXC Browser socket does not exists")
+ self.client_id = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8')
+ self.sock.connect(self.socket_path)
+
+ self.send_raw_msg(dict(
+ action = 'change-public-keys',
+ publicKey = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
+ nonce = base64.b64encode(nacl.utils.random(nacl.public.Box.NONCE_SIZE)).decode('utf-8'),
+ clientID = self.client_id
+ ))
+
+ resp = self.recv_raw_msg()
+ assert resp['action'] == 'change-public-keys'
+ assert resp['success'] == 'true'
+ assert resp['nonce']
+ self.cryptobox = nacl.public.Box(
+ self.client_key,
+ nacl.public.PublicKey(base64.b64decode(resp['publicKey']))
+ )
+
+ def get_databasehash(self):
+ self.send_msg(dict(action='get-databasehash'))
+ return self.recv_msg()['hash']
+
+ def lock_database(self):
+ self.send_msg(dict(action='lock-database'))
+ try:
+ self.recv_msg()
+ except KeepassError as e:
+ if e.code == 1:
+ return True
+ raise
+ return False
+
+
+ def test_associate(self):
+ if not self.id:
+ return False
+ self.send_msg(dict(
+ action = 'test-associate',
+ id = self.id,
+ key = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
+ ))
+ return self.recv_msg()['success'] == 'true'
+
+ def associate(self):
+ self.send_msg(dict(
+ action = 'associate',
+ key = base64.b64encode(self.client_key.public_key.encode()).decode('utf-8'),
+ idKey = base64.b64encode(self.id_key.public_key.encode()).decode('utf-8')
+ ))
+ resp = self.recv_msg()
+ self.id = resp['id']
+
+ def get_logins(self, url):
+ self.send_msg(dict(
+ action = 'get-logins',
+ url = url,
+ keys = [{ 'id': self.id, 'key': base64.b64encode(self.id_key.public_key.encode()).decode('utf-8') }]
+ ))
+ return self.recv_msg()['entries']
+
+ def send_raw_msg(self, msg):
+ self.sock.send( json.dumps(msg).encode('utf-8') )
+
+ def recv_raw_msg(self):
+ return json.loads( self.sock.recv(4096).decode('utf-8') )
+
+ def send_msg(self, msg, **extra):
+ nonce = nacl.utils.random(nacl.public.Box.NONCE_SIZE)
+ self.send_raw_msg(dict(
+ action = msg['action'],
+ message = base64.b64encode(self.cryptobox.encrypt(json.dumps(msg).encode('utf-8'), nonce).ciphertext).decode('utf-8'),
+ nonce = base64.b64encode(nonce).decode('utf-8'),
+ clientID = self.client_id,
+ **extra
+ ))
+
+ def recv_msg(self):
+ resp = self.recv_raw_msg()
+ if 'error' in resp:
+ raise KeepassError(resp['errorCode'], resp['error'])
+ assert resp['action']
+ return json.loads(self.cryptobox.decrypt(base64.b64decode(resp['message']), base64.b64decode(resp['nonce'])).decode('utf-8'))
+
+
+
+class SecretKeyStore:
+ def __init__(self, gpgkey):
+ self.gpgkey = gpgkey
+ if gpgkey is None:
+ self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key')
+ else:
+ self.path = os.path.join(os.environ['QUTE_DATA_DIR'], 'keepassxc.key.gpg')
+
+ def load(self):
+ "Load existing association key from file"
+ if self.gpgkey is None:
+ jsondata = open(self.path, 'r').read()
+ else:
+ jsondata = subprocess.check_output(['gpg', '--decrypt', self.path]).decode('utf-8')
+ data = json.loads(jsondata)
+ self.id = data['id']
+ self.key = base64.b64decode(data['key'])
+
+ def create(self):
+ "Create new association key"
+ self.key = nacl.utils.random(32)
+ self.id = None
+
+ def store(self, id):
+ "Store newly created association key in file"
+ self.id = id
+ jsondata = json.dumps({'id':self.id, 'key':base64.b64encode(self.key).decode('utf-8')})
+ if self.gpgkey is None:
+ open(self.path, "w").write(jsondata)
+ else:
+ subprocess.run(['gpg', '--encrypt', '-o', self.path, '-r', self.gpgkey], input=jsondata.encode('utf-8'), check=True)
+
+
+def qute(cmd):
+ with open(os.environ['QUTE_FIFO'], 'w') as fifo:
+ fifo.write(cmd)
+ fifo.write('\n')
+ fifo.flush()
+
+def error(msg):
+ print(msg, file=sys.stderr)
+ qute('message-error "{}"'.format(msg))
+
+
+def connect_to_keepassxc(args):
+ assert args.key or args.insecure, "Missing GPG key to use for auth key encryption"
+ keystore = SecretKeyStore(args.key)
+ if os.path.isfile(keystore.path):
+ keystore.load()
+ kp = KeepassXC(keystore.id, key=keystore.key, socket_path=args.socket)
+ kp.connect()
+ if not kp.test_associate():
+ error('No KeepassXC association')
+ return None
+ else:
+ keystore.create()
+ kp = KeepassXC(key=keystore.key, socket_path=args.socket)
+ kp.connect()
+ kp.associate()
+ if not kp.test_associate():
+ error('No KeepassXC association')
+ return None
+ keystore.store(kp.id)
+ return kp
+
+
+def make_js_code(username, password):
+ return ' '.join("""
+ function isVisible(elem) {
+ var style = elem.ownerDocument.defaultView.getComputedStyle(elem, null);
+
+ if (style.getPropertyValue("visibility") !== "visible" ||
+ style.getPropertyValue("display") === "none" ||
+ style.getPropertyValue("opacity") === "0") {
+ return false;
+ }
+
+ return elem.offsetWidth > 0 && elem.offsetHeight > 0;
+ };
+
+ function hasPasswordField(form) {
+ var inputs = form.getElementsByTagName("input");
+ for (var j = 0; j < inputs.length; j++) {
+ var input = inputs[j];
+ if (input.type === "password") {
+ return true;
+ }
+ }
+ return false;
+ };
+
+ function loadData2Form (form) {
+ var inputs = form.getElementsByTagName("input");
+ for (var j = 0; j < inputs.length; j++) {
+ var input = inputs[j];
+ if (isVisible(input) && (input.type === "text" || input.type === "email")) {
+ input.focus();
+ input.value = %s;
+ input.dispatchEvent(new Event('input', { 'bubbles': true }));
+ input.dispatchEvent(new Event('change', { 'bubbles': true }));
+ input.blur();
+ }
+ if (input.type === "password") {
+ input.focus();
+ input.value = %s;
+ input.dispatchEvent(new Event('input', { 'bubbles': true }));
+ input.dispatchEvent(new Event('change', { 'bubbles': true }));
+ input.blur();
+ }
+ }
+ };
+
+ function fillFirstForm() {
+ var forms = document.getElementsByTagName("form");
+ for (i = 0; i < forms.length; i++) {
+ if (hasPasswordField(forms[i])) {
+ loadData2Form(forms[i]);
+ return;
+ }
+ }
+ alert("No Credentials Form found");
+ };
+
+ fillFirstForm()
+ """.splitlines()) % (json.dumps(username), json.dumps(password))
+
+
+def main():
+ if 'QUTE_FIFO' not in os.environ:
+ print(f"No QUTE_FIFO found - {sys.argv[0]} must be run as a qutebrowser userscript")
+ sys.exit(-1)
+
+ try:
+ args = parse_args()
+ assert args.url, "Missing URL"
+ kp = connect_to_keepassxc(args)
+ if not kp:
+ error('Could not connect to KeepassXC')
+ return
+ creds = kp.get_logins(args.url)
+ if not creds:
+ error('No credentials found')
+ return
+ # TODO: handle multiple matches
+ name, pw = creds[0]['login'], creds[0]['password']
+ if name and pw:
+ qute('jseval -q ' + make_js_code(name, pw))
+ except Exception as e:
+ error(str(e))
+
+
+if __name__ == '__main__':
+ main()
+
diff --git a/misc/userscripts/readability b/misc/userscripts/readability
index f9cbbf829..a6a6f2d52 100755
--- a/misc/userscripts/readability
+++ b/misc/userscripts/readability
@@ -57,6 +57,9 @@ with codecs.open(os.environ['QUTE_HTML'], 'r', 'utf-8') as source:
title = doc.title()
content = doc.summary().replace('<html>', HEADER % title)
+ # add a class to make styling the page easier
+ content = content.replace('<body>', '<body class="qute-readability">')
+
with codecs.open(tmpfile, 'w', 'utf-8') as target:
target.write(content.lstrip())
diff --git a/misc/userscripts/readability-js b/misc/userscripts/readability-js
index 2f24e065d..532df51c6 100755
--- a/misc/userscripts/readability-js
+++ b/misc/userscripts/readability-js
@@ -131,6 +131,9 @@ getDOM(target, domOpts).then(dom => {
let article = reader.parse();
let content = util.format(HEADER, article.title) + article.content;
+ // add a class to make styling the page easier
+ content = content.replace('<body>', '<body class="qute-readability">')
+
fs.writeFile(tmpFile, content, (err) => {
if (err) {
qute.messageError([`"${err}"`])
diff --git a/pytest.ini b/pytest.ini
index d0f41948b..7f4a58de3 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -34,7 +34,6 @@ markers =
no_invalid_lines: Don't fail on unparsable lines in end2end tests
fake_os: Fake utils.is_* to a fake operating system
unicode_locale: Tests which need a unicode locale to work
- qtwebkit6021_xfail: Tests which would fail on WebKit version 602.1
js_headers: Sets JS headers dynamically on QtWebEngine (unsupported on some versions)
qtwebkit_pdf_imageformat_skip: Broken on QtWebKit with PDF image format plugin installed
windows_skip: Tests which should be skipped on Windows
diff --git a/qutebrowser/browser/browsertab.py b/qutebrowser/browser/browsertab.py
index 4c26da69d..581d33507 100644
--- a/qutebrowser/browser/browsertab.py
+++ b/qutebrowser/browser/browsertab.py
@@ -870,7 +870,7 @@ class AbstractTabPrivate:
tabdata = self._tab.data
if tabdata.inspector is None:
assert tabdata.splitter is not None
- tabdata.inspector = inspector.create(
+ tabdata.inspector = self._init_inspector(
splitter=tabdata.splitter,
win_id=self._tab.win_id)
self._tab.shutting_down.connect(tabdata.inspector.shutdown)
@@ -878,6 +878,18 @@ class AbstractTabPrivate:
tabdata.inspector.inspect(self._widget.page())
tabdata.inspector.set_position(position)
+ def _init_inspector(self, splitter: 'miscwidgets.InspectorSplitter',
+ win_id: int,
+ parent: QWidget = None) -> 'AbstractWebInspector':
+ """Get a WebKitInspector/WebEngineInspector.
+
+ Args:
+ splitter: InspectorSplitter where the inspector can be placed.
+ win_id: The window ID this inspector is associated with.
+ parent: The Qt parent to set.
+ """
+ raise NotImplementedError
+
class AbstractTab(QWidget):
diff --git a/qutebrowser/browser/commands.py b/qutebrowser/browser/commands.py
index dc0664238..f2dd282df 100644
--- a/qutebrowser/browser/commands.py
+++ b/qutebrowser/browser/commands.py
@@ -329,7 +329,9 @@ class CommandDispatcher:
# Explicit count with a tab that doesn't exist.
return
elif curtab.navigation_blocked():
- message.info("Tab is pinned!")
+ message.info("Tab is pinned! Opening in new tab.")
+ self._tabbed_browser.tabopen(cur_url)
+
else:
curtab.load_url(cur_url)
diff --git a/qutebrowser/browser/inspector.py b/qutebrowser/browser/inspector.py
index 9ed5b52ea..2b40e97e4 100644
--- a/qutebrowser/browser/inspector.py
+++ b/qutebrowser/browser/inspector.py
@@ -30,30 +30,9 @@ from PyQt5.QtGui import QCloseEvent
from qutebrowser.browser import eventfilter
from qutebrowser.config import configfiles
-from qutebrowser.utils import log, usertypes, utils
+from qutebrowser.utils import log, usertypes
from qutebrowser.keyinput import modeman
-from qutebrowser.misc import miscwidgets, objects
-
-
-def create(*, splitter: 'miscwidgets.InspectorSplitter',
- win_id: int,
- parent: QWidget = None) -> 'AbstractWebInspector':
- """Get a WebKitInspector/WebEngineInspector.
-
- Args:
- splitter: InspectorSplitter where the inspector can be placed.
- win_id: The window ID this inspector is associated with.
- parent: The Qt parent to set.
- """
- # Importing modules here so we don't depend on QtWebEngine without the
- # argument and to avoid circular imports.
- if objects.backend == usertypes.Backend.QtWebEngine:
- from qutebrowser.browser.webengine import webengineinspector
- return webengineinspector.WebEngineInspector(splitter, win_id, parent)
- elif objects.backend == usertypes.Backend.QtWebKit:
- from qutebrowser.browser.webkit import webkitinspector
- return webkitinspector.WebKitInspector(splitter, win_id, parent)
- raise utils.Unreachable(objects.backend)
+from qutebrowser.misc import miscwidgets
class Position(enum.Enum):
diff --git a/qutebrowser/browser/shared.py b/qutebrowser/browser/shared.py
index 78b475835..94332ffcb 100644
--- a/qutebrowser/browser/shared.py
+++ b/qutebrowser/browser/shared.py
@@ -23,7 +23,7 @@ import os
import sys
import html
import netrc
-from typing import Callable, Mapping, List
+from typing import Callable, Mapping, List, Optional
import tempfile
from PyQt5.QtCore import QUrl
@@ -360,23 +360,60 @@ def choose_file(multiple: bool) -> List[str]:
A list of selected file paths, or empty list if no file is selected.
If multiple is False, the return value will have at most 1 item.
"""
- handle = tempfile.NamedTemporaryFile(prefix='qutebrowser-fileselect-', delete=False)
- handle.close()
- tmpfilename = handle.name
- with utils.cleanup_file(tmpfilename):
- if multiple:
- command = config.val.fileselect.multiple_files.command
- else:
- command = config.val.fileselect.single_file.command
+ if multiple:
+ command = config.val.fileselect.multiple_files.command
+ else:
+ command = config.val.fileselect.single_file.command
+ use_tmp_file = any('{}' in arg for arg in command[1:])
+ if use_tmp_file:
+ handle = tempfile.NamedTemporaryFile(
+ prefix='qutebrowser-fileselect-',
+ delete=False,
+ )
+ handle.close()
+ tmpfilename = handle.name
+ with utils.cleanup_file(tmpfilename):
+ command = (
+ command[:1] +
+ [arg.replace('{}', tmpfilename) for arg in command[1:]]
+ )
+ return _execute_fileselect_command(
+ command=command,
+ multiple=multiple,
+ tmpfilename=tmpfilename,
+ )
+ else:
+ return _execute_fileselect_command(
+ command=command,
+ multiple=multiple,
+ )
- proc = guiprocess.GUIProcess(what='choose-file')
- proc.start(command[0],
- [arg.replace('{}', tmpfilename) for arg in command[1:]])
- loop = qtutils.EventLoop()
- proc.finished.connect(lambda _code, _status: loop.exit())
- loop.exec()
+def _execute_fileselect_command(
+ command: List[str],
+ multiple: bool,
+ tmpfilename: Optional[str] = None
+) -> List[str]:
+ """Execute external command to choose file.
+ Args:
+ multiple: Should selecting multiple files be allowed.
+ tmpfilename: Path to the temporary file if used, otherwise None.
+
+ Return:
+ A list of selected file paths, or empty list if no file is selected.
+ If multiple is False, the return value will have at most 1 item.
+ """
+ proc = guiprocess.GUIProcess(what='choose-file')
+ proc.start(command[0], command[1:])
+
+ loop = qtutils.EventLoop()
+ proc.finished.connect(lambda _code, _status: loop.exit())
+ loop.exec()
+
+ if tmpfilename is None:
+ selected_files = proc.final_stdout.splitlines()
+ else:
try:
with open(tmpfilename, mode='r', encoding=sys.getfilesystemencoding()) as f:
selected_files = f.read().splitlines()
diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py
index 9f129b609..450d68751 100644
--- a/qutebrowser/browser/webengine/webenginetab.py
+++ b/qutebrowser/browser/webengine/webenginetab.py
@@ -34,11 +34,13 @@ from PyQt5.QtWebEngineWidgets import QWebEnginePage, QWebEngineScript, QWebEngin
from qutebrowser.config import config
from qutebrowser.browser import browsertab, eventfilter, shared, webelem, greasemonkey
from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
- webenginesettings, certificateerror)
-from qutebrowser.misc import miscwidgets, objects
+ webenginesettings, certificateerror,
+ webengineinspector)
+
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
message, jinja, debug, version)
from qutebrowser.qt import sip
+from qutebrowser.misc import objects, miscwidgets
# Mapping worlds from usertypes.JsWorld to QWebEngineScript world IDs.
@@ -1078,18 +1080,11 @@ class _WebEngineScripts(QObject):
removed = page_scripts.remove(script)
assert removed, script.name()
- def _inject_greasemonkey_scripts(self, scripts=None, injection_point=None,
- remove_first=True):
+ def _inject_greasemonkey_scripts(self, scripts):
"""Register user JavaScript files with the current tab.
Args:
- scripts: A list of GreasemonkeyScripts, or None to add all
- known by the Greasemonkey subsystem.
- injection_point: The QWebEngineScript::InjectionPoint stage
- to inject the script into, None to use
- auto-detection.
- remove_first: Whether to remove all previously injected
- scripts before adding these ones.
+ scripts: A list of GreasemonkeyScripts.
"""
if sip.isdeleted(self._widget):
return
@@ -1100,49 +1095,49 @@ class _WebEngineScripts(QObject):
# 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()
- if remove_first:
- self._remove_all_greasemonkey_scripts()
-
- if not scripts:
- return
+ self._remove_all_greasemonkey_scripts()
for script in scripts:
new_script = QWebEngineScript()
+
try:
world = int(script.jsworld)
if not 0 <= world <= qtutils.MAX_WORLD_ID:
log.greasemonkey.error(
- "script {} has invalid value for '@qute-js-world'"
- ": {}, should be between 0 and {}"
- .format(
- script.name,
- script.jsworld,
- qtutils.MAX_WORLD_ID))
+ 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()]]
+ world = _JS_WORLD_MAP[usertypes.JsWorld[script.jsworld.lower()]]
except KeyError:
log.greasemonkey.error(
- "script {} has invalid value for '@qute-js-world'"
- ": {}".format(script.name, script.jsworld))
+ 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.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.DocumentReady)
+
new_script.setSourceCode(script.code())
- new_script.setName("GM-{}".format(script.name))
+ new_script.setName(f"GM-{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.needs_document_end_workaround():
- log.greasemonkey.debug("Forcing @run-at document-end for {}"
- .format(script.name))
+ if script.needs_document_end_workaround():
+ log.greasemonkey.debug(
+ f"Forcing @run-at document-end for {script.name}")
new_script.setInjectionPoint(QWebEngineScript.DocumentReady)
- log.greasemonkey.debug('adding script: {}'
- .format(new_script.name()))
+ log.greasemonkey.debug(f'adding script: {new_script.name()}')
page_scripts.insert(new_script)
def _inject_site_specific_quirks(self):
@@ -1207,6 +1202,9 @@ class WebEngineTabPrivate(browsertab.AbstractTabPrivate):
def run_js_sync(self, code):
raise browsertab.UnsupportedOperationError
+ def _init_inspector(self, splitter, win_id, parent=None):
+ return webengineinspector.WebEngineInspector(splitter, win_id, parent)
+
class WebEngineTab(browsertab.AbstractTab):
diff --git a/qutebrowser/browser/webkit/webkittab.py b/qutebrowser/browser/webkit/webkittab.py
index 066cce348..f910cf676 100644
--- a/qutebrowser/browser/webkit/webkittab.py
+++ b/qutebrowser/browser/webkit/webkittab.py
@@ -33,7 +33,8 @@ from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab, shared
from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
- webkitsettings)
+ webkitsettings, webkitinspector)
+
from qutebrowser.utils import qtutils, usertypes, utils, log, debug
from qutebrowser.keyinput import modeman
from qutebrowser.qt import sip
@@ -808,6 +809,9 @@ class WebKitTabPrivate(browsertab.AbstractTabPrivate):
result = document_element.evaluateJavaScript(code)
return result
+ def _init_inspector(self, splitter, win_id, parent=None):
+ return webkitinspector.WebKitInspector(splitter, win_id, parent)
+
class WebKitTab(browsertab.AbstractTab):
diff --git a/qutebrowser/config/configdata.yml b/qutebrowser/config/configdata.yml
index b4805665b..34d8bec96 100644
--- a/qutebrowser/config/configdata.yml
+++ b/qutebrowser/config/configdata.yml
@@ -1259,7 +1259,7 @@ fileselect.handler:
fileselect.single_file.command:
type:
name: ShellCommand
- placeholder: true
+ placeholder: false
completions:
- ['["xterm", "-e", "ranger", "--choosefile={}"]', "Ranger in xterm"]
- ['["xterm", "-e", "vifm", "--choose-files", "{}"]', "vifm in xterm"]
@@ -1267,16 +1267,17 @@ fileselect.single_file.command:
default: ['xterm', '-e', 'ranger', '--choosefile={}']
desc: >-
Command (and arguments) to use for selecting a single file in forms.
- The command should write the selected file path to the specified file.
+ The command should write the selected file path to the specified file or stdout.
The following placeholders are defined:
- * `{}`: Filename of the file to be written to.
+ * `{}`: Filename of the file to be written to. If not contained in any argument, the
+ standard output of the command is read instead.
fileselect.multiple_files.command:
type:
name: ShellCommand
- placeholder: true
+ placeholder: false
completions:
- ['["xterm", "-e", "ranger", "--choosefiles={}"]', "Ranger in xterm"]
- ['["xterm", "-e", "vifm", "--choose-files", "{}"]', "vifm in xterm"]
@@ -1284,12 +1285,13 @@ fileselect.multiple_files.command:
default: ['xterm', '-e', 'ranger', '--choosefiles={}']
desc: >-
Command (and arguments) to use for selecting multiple files in forms.
- The command should write the selected file paths to the specified file,
+ The command should write the selected file paths to the specified file or to stdout,
separated by newlines.
The following placeholders are defined:
- * `{}`: Filename of the file to be written to.
+ * `{}`: Filename of the file to be written to. If not contained in any argument, the
+ standard output of the command is read instead.
## hints
diff --git a/qutebrowser/keyinput/modeman.py b/qutebrowser/keyinput/modeman.py
index 08c5a151b..c00120596 100644
--- a/qutebrowser/keyinput/modeman.py
+++ b/qutebrowser/keyinput/modeman.py
@@ -86,9 +86,10 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
hintmanager = hints.HintManager(win_id, parent=parent)
objreg.register('hintmanager', hintmanager, scope='window',
window=win_id, command_only=True)
-
modeman.hintmanager = hintmanager
+ log_sensitive_keys = 'log-sensitive-keys' in objects.debug_flags
+
keyparsers: ParserDictType = {
usertypes.KeyMode.normal:
modeparsers.NormalKeyParser(
@@ -110,7 +111,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
- do_log=False,
+ do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.passthrough:
@@ -120,7 +121,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
- do_log=False,
+ do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.command:
@@ -130,7 +131,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
- do_log=False,
+ do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.prompt:
@@ -140,7 +141,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
- do_log=False,
+ do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.yesno:
diff --git a/qutebrowser/mainwindow/tabbedbrowser.py b/qutebrowser/mainwindow/tabbedbrowser.py
index 52241d777..8d2801d31 100644
--- a/qutebrowser/mainwindow/tabbedbrowser.py
+++ b/qutebrowser/mainwindow/tabbedbrowser.py
@@ -929,16 +929,12 @@ class TabbedBrowser(QWidget):
return
messages = {
- browsertab.TerminationStatus.abnormal:
- "Renderer process exited with status {}".format(code),
- browsertab.TerminationStatus.crashed:
- "Renderer process crashed",
- browsertab.TerminationStatus.killed:
- "Renderer process was killed",
- browsertab.TerminationStatus.unknown:
- "Renderer process did not start",
+ browsertab.TerminationStatus.abnormal: "Renderer process exited",
+ browsertab.TerminationStatus.crashed: "Renderer process crashed",
+ browsertab.TerminationStatus.killed: "Renderer process was killed",
+ browsertab.TerminationStatus.unknown: "Renderer process did not start",
}
- msg = messages[status]
+ msg = messages[status] + f" (status {code})"
def show_error_page(html):
tab.set_html(html)
diff --git a/qutebrowser/misc/guiprocess.py b/qutebrowser/misc/guiprocess.py
index 1273b227e..79c84c346 100644
--- a/qutebrowser/misc/guiprocess.py
+++ b/qutebrowser/misc/guiprocess.py
@@ -61,6 +61,9 @@ class GUIProcess(QObject):
self.cmd = None
self.args = None
+ self.final_stdout: str = ""
+ self.final_stderr: str = ""
+
self._proc = QProcess(self)
self._proc.errorOccurred.connect(self._on_error)
self._proc.errorOccurred.connect(self.error)
@@ -125,6 +128,8 @@ class GUIProcess(QObject):
log.procs.error("Process stderr:\n" + stderr.strip())
qutescheme.spawn_output = self._spawn_format(exitinfo, stdout, stderr)
+ self.final_stdout = stdout
+ self.final_stderr = stderr
def _spawn_format(self, exitinfo, stdout, stderr):
"""Produce a formatted string for spawn output."""
diff --git a/qutebrowser/qutebrowser.py b/qutebrowser/qutebrowser.py
index 64c175293..9e1fb91cd 100644
--- a/qutebrowser/qutebrowser.py
+++ b/qutebrowser/qutebrowser.py
@@ -173,6 +173,7 @@ def debug_flag_error(flag):
log-requests: Log all network requests.
log-cookies: Log cookies in cookie filter.
log-scroll-pos: Log all scrolling changes.
+ log-sensitive-keys: Log keypresses in passthrough modes.
stack: Enable Chromium stack logging.
chromium: Enable Chromium logging.
wait-renderer-process: Wait for debugger in renderer process.
@@ -181,7 +182,7 @@ def debug_flag_error(flag):
"""
valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history',
'no-scroll-filtering', 'log-requests', 'log-cookies',
- 'log-scroll-pos', 'stack', 'chromium',
+ 'log-scroll-pos', 'log-sensitive-keys', 'stack', 'chromium',
'wait-renderer-process', 'avoid-chromium-init', 'werror']
if flag in valid_flags:
diff --git a/requirements.txt b/requirements.txt
index c6eb86d6f..5572e206c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,11 +3,11 @@
adblock==0.4.2 ; python_version!="3.10"
colorama==0.4.4
dataclasses==0.6 ; python_version<"3.7"
-importlib-metadata==3.7.0 ; python_version<"3.8"
-importlib-resources==5.1.1 ; python_version<"3.9"
+importlib-metadata==3.7.2 ; python_version<"3.8"
+importlib-resources==5.1.2 ; python_version<"3.9"
Jinja2==2.11.3
MarkupSafe==1.1.1
-Pygments==2.8.0
+Pygments==2.8.1
PyYAML==5.4.1
typing-extensions==3.7.4.3
-zipp==3.4.0
+zipp==3.4.1
diff --git a/scripts/dev/ci/docker/Dockerfile.j2 b/scripts/dev/ci/docker/Dockerfile.j2
index 03e5684ad..d3fc82793 100644
--- a/scripts/dev/ci/docker/Dockerfile.j2
+++ b/scripts/dev/ci/docker/Dockerfile.j2
@@ -1,12 +1,5 @@
FROM archlinux:latest
-# WORKAROUND for glibc 2.33 and old Docker
-# See https://github.com/actions/virtual-environments/issues/2658
-# Thanks to https://github.com/lxqt/lxqt-panel/pull/1562
-RUN patched_glibc=glibc-linux4-2.33-4-x86_64.pkg.tar.zst && \
- curl -LO "https://repo.archlinuxcn.org/x86_64/$patched_glibc" && \
- bsdtar -C / -xvf "$patched_glibc"
-
{% if unstable %}
RUN sed -i '/^# after the header/a[kde-unstable]\nInclude = /etc/pacman.d/mirrorlist\n\n[testing]\nInclude = /etc/pacman.d/mirrorlist' /etc/pacman.conf
{% endif %}
diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py
index 8895da55f..612b88637 100755
--- a/scripts/dev/run_vulture.py
+++ b/scripts/dev/run_vulture.py
@@ -61,6 +61,7 @@ def whitelist_generator(): # noqa: C901
yield 'scripts.utils.bg_colors'
yield 'qutebrowser.misc.sql.SqliteErrorCode.CONSTRAINT'
yield 'qutebrowser.misc.throttle.Throttle.set_delay'
+ yield 'qutebrowser.misc.guiprocess.GUIProcess.final_stderr'
# Qt attributes
yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().baseUrl'
diff --git a/tests/conftest.py b/tests/conftest.py
index ea7381a2f..ee945ac4c 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -109,12 +109,6 @@ def _apply_platform_markers(config, item):
pytest.mark.skipif,
sys.getfilesystemencoding() == 'ascii',
"Skipped because of ASCII locale"),
-
- ('qtwebkit6021_xfail',
- pytest.mark.xfail,
- version.qWebKitVersion and # type: ignore[unreachable]
- version.qWebKitVersion() == '602.1',
- "Broken on WebKit 602.1")
]
for searched_marker, new_marker_kind, condition, default_reason in markers:
diff --git a/tests/end2end/features/editor.feature b/tests/end2end/features/editor.feature
index 52756422c..47cb1230a 100644
--- a/tests/end2end/features/editor.feature
+++ b/tests/end2end/features/editor.feature
@@ -6,14 +6,14 @@ Feature: Opening external editors
Scenario: Editing a URL
When I open data/numbers/1.txt
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url
Then data/numbers/2.txt should be loaded
Scenario: Editing a URL with -t
When I run :tab-only
And I open data/numbers/1.txt
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -t
Then data/numbers/2.txt should be loaded
And the following tabs should be open:
@@ -24,7 +24,7 @@ Feature: Opening external editors
When I set tabs.new_position.related to prev
And I open data/numbers/1.txt
And I run :tab-only
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -rt
Then data/numbers/2.txt should be loaded
And the following tabs should be open:
@@ -34,7 +34,7 @@ Feature: Opening external editors
Scenario: Editing a URL with -b
When I run :tab-only
And I open data/numbers/1.txt
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -b
Then data/numbers/2.txt should be loaded
And the following tabs should be open:
@@ -45,7 +45,7 @@ Feature: Opening external editors
When I run :window-only
And I open data/numbers/1.txt in a new tab
And I run :tab-only
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -w
Then data/numbers/2.txt should be loaded
And the session should look like:
@@ -65,7 +65,7 @@ Feature: Opening external editors
When I open data/numbers/1.txt in a new tab
And I run :tab-only
And I run :window-only
- And I set up a fake editor replacing "1.txt" by "2.txt"
+ And I setup a fake editor replacing "1.txt" by "2.txt"
And I run :edit-url -p
Then data/numbers/2.txt should be loaded
And the session should look like:
@@ -90,13 +90,13 @@ Feature: Opening external editors
Scenario: Editing a URL with invalid URL
When I set url.auto_search to never
And I open data/hello.txt
- And I set up a fake editor replacing "http://localhost:(port)/data/hello.txt" by "foo!"
+ And I setup a fake editor replacing "http://localhost:(port)/data/hello.txt" by "foo!"
And I run :edit-url
Then the error "Invalid URL" should be shown
Scenario: Spawning an editor successfully
Given I have a fresh instance
- When I set up a fake editor returning "foobar"
+ When I setup a fake editor returning "foobar"
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -105,7 +105,7 @@ Feature: Opening external editors
Then the javascript message "text: foobar" should be logged
Scenario: Spawning an editor in normal mode
- When I set up a fake editor returning "foobar"
+ When I setup a fake editor returning "foobar"
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -119,7 +119,7 @@ Feature: Opening external editors
# There's no guarantee that the tab gets deleted...
@posix
Scenario: Spawning an editor and closing the tab
- When I set up a fake editor that writes "foobar" on save
+ When I setup a fake editor that writes "foobar" on save
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -134,7 +134,7 @@ Feature: Opening external editors
# Could not get signals working on Windows
@posix
Scenario: Spawning an editor and saving
- When I set up a fake editor that writes "foobar" on save
+ When I setup a fake editor that writes "foobar" on save
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -145,7 +145,7 @@ Feature: Opening external editors
Then the javascript message "text: foobar" should be logged
Scenario: Spawning an editor in caret mode
- When I set up a fake editor returning "foobar"
+ When I setup a fake editor returning "foobar"
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -159,7 +159,7 @@ Feature: Opening external editors
Then the javascript message "text: foobar" should be logged
Scenario: Spawning an editor with existing text
- When I set up a fake editor replacing "foo" by "bar"
+ When I setup a fake editor replacing "foo" by "bar"
And I open data/editor.html
And I run :click-element id qute-textarea
And I wait for "Entering mode KeyMode.insert (reason: clicking input)" in the log
@@ -173,20 +173,20 @@ Feature: Opening external editors
Scenario: Edit a command and run it
When I run :set-cmd-text :message-info foo
- And I set up a fake editor replacing "foo" by "bar"
+ And I setup a fake editor replacing "foo" by "bar"
And I run :edit-command --run
Then the message "bar" should be shown
And "Leaving mode KeyMode.command (reason: cmd accept)" should be logged
Scenario: Edit a command and omit the start char
- When I set up a fake editor returning "message-info foo"
+ When I setup a fake editor returning "message-info foo"
And I run :edit-command
Then the error "command must start with one of :/?" should be shown
And "Leaving mode KeyMode.command *" should not be logged
Scenario: Edit a command to be empty
When I run :set-cmd-text :
- When I set up a fake editor returning empty text
+ When I setup a fake editor returning empty text
And I run :edit-command
Then the error "command must start with one of :/?" should be shown
And "Leaving mode KeyMode.command *" should not be logged
@@ -194,13 +194,20 @@ Feature: Opening external editors
## select single file
Scenario: Select one file with single command
- When I set up a fake "single_file" fileselector selecting "tests/end2end/data/numbers/1.txt"
+ When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers/1.txt" and writes to a temporary file
+ And I open data/fileselect.html
+ And I run :click-element id single_file
+ Then the javascript message "Files: 1.txt" should be logged
+
+ Scenario: Select one file with single command that writes to stdout
+ When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers/1.txt" and writes to stdout
And I open data/fileselect.html
And I run :click-element id single_file
Then the javascript message "Files: 1.txt" should be logged
Scenario: Select two files with single command
- When I set up a fake "single_file" fileselector selecting "tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/2.txt"
+ When I setup a fake single_file fileselector selecting "tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/2.txt" and writes to a temporary file
+
And I open data/fileselect.html
And I run :click-element id single_file
Then the javascript message "Files: 1.txt" should be logged
@@ -209,13 +216,15 @@ Feature: Opening external editors
## select multiple files
Scenario: Select one file with multiple command
- When I set up a fake "multiple_files" fileselector selecting "tests/end2end/data/numbers/1.txt"
+ When I setup a fake multiple_files fileselector selecting "tests/end2end/data/numbers/1.txt" and writes to a temporary file
+
And I open data/fileselect.html
And I run :click-element id multiple_files
Then the javascript message "Files: 1.txt" should be logged
Scenario: Select two files with multiple command
- When I set up a fake "multiple_files" fileselector selecting "tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/2.txt"
+ When I setup a fake multiple_files fileselector selecting "tests/end2end/data/numbers/1.txt tests/end2end/data/numbers/2.txt" and writes to a temporary file
+
And I open data/fileselect.html
And I run :click-element id multiple_files
Then the javascript message "Files: 1.txt, 2.txt" should be logged
diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature
index 351135fab..e6a02e038 100644
--- a/tests/end2end/features/misc.feature
+++ b/tests/end2end/features/misc.feature
@@ -140,7 +140,7 @@ Feature: Various utility commands.
Scenario: :jseval --file using a file that doesn't exist as js-code
When I run :jseval --file /nonexistentfile
- Then the error "[Errno 2] No such file or directory: '/nonexistentfile'" should be shown
+ Then the error "[Errno 2] *: '/nonexistentfile'" should be shown
And "No output or error" should not be logged
# :debug-webaction
@@ -528,13 +528,13 @@ Feature: Various utility commands.
@qtwebkit_skip @no_invalid_lines @posix
Scenario: Renderer crash
When I run :open -t chrome://crash
- Then "Renderer process crashed" should be logged
+ Then "Renderer process crashed (status *)" should be logged
And "* 'Error loading chrome://crash/'" should be logged
@qtwebkit_skip @no_invalid_lines @flaky
Scenario: Renderer kill
When I run :open -t chrome://kill
- Then "Renderer process was killed" should be logged
+ Then "Renderer process was killed (status *)" should be logged
And "* 'Error loading chrome://kill/'" should be logged
# https://github.com/qutebrowser/qutebrowser/issues/2290
@@ -544,7 +544,7 @@ Feature: Various utility commands.
And I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I run :open chrome://kill
- And I wait for "Renderer process was killed" in the log
+ And I wait for "Renderer process was killed (status *)" in the log
And I open data/numbers/3.txt
Then no crash should happen
@@ -554,11 +554,11 @@ Feature: Various utility commands.
When I open data/crashers/webrtc.html in a new tab
And I run :reload
And I wait until data/crashers/webrtc.html is loaded
- Then "Renderer process crashed" should not be logged
+ Then "Renderer process crashed (status *)" should not be logged
Scenario: InstalledApps crash
When I open data/crashers/installedapp.html in a new tab
- Then "Renderer process was killed" should not be logged
+ Then "Renderer process was killed (status *)" should not be logged
## Other
diff --git a/tests/end2end/features/qutescheme.feature b/tests/end2end/features/qutescheme.feature
index 286f8f80a..1424bbf09 100644
--- a/tests/end2end/features/qutescheme.feature
+++ b/tests/end2end/features/qutescheme.feature
@@ -215,7 +215,7 @@ Feature: Special qute:// pages
Scenario: Running :pyeval --file using a non existing file
When I run :debug-pyeval --file nonexistentfile
- Then the error "[Errno 2] No such file or directory: 'nonexistentfile'" should be shown
+ Then the error "[Errno 2] *: 'nonexistentfile'" should be shown
Scenario: Running :pyeval with --quiet
When I run :debug-pyeval --quiet 1+1
diff --git a/tests/end2end/features/sessions.feature b/tests/end2end/features/sessions.feature
index 0f0c015e0..e48947cbd 100644
--- a/tests/end2end/features/sessions.feature
+++ b/tests/end2end/features/sessions.feature
@@ -395,9 +395,10 @@ Feature: Saving and loading sessions
And I run :session-load -c pin_session
And I wait until data/numbers/3.txt is loaded
And I run :tab-focus 2
- And I run :open hello world
- Then the message "Tab is pinned!" should be shown
+ And I open data/numbers/4.txt
+ Then the message "Tab is pinned! Opening in new tab." should be shown
And the following tabs should be open:
- data/numbers/1.txt
- data/numbers/2.txt (active) (pinned)
+ - data/numbers/4.txt
- data/numbers/3.txt
diff --git a/tests/end2end/features/tabs.feature b/tests/end2end/features/tabs.feature
index ca0efefc4..7db054573 100644
--- a/tests/end2end/features/tabs.feature
+++ b/tests/end2end/features/tabs.feature
@@ -1534,10 +1534,11 @@ Feature: Tab management
Scenario: :tab-pin open url
When I open data/numbers/1.txt
And I run :tab-pin
- And I open data/numbers/2.txt without waiting
- Then the message "Tab is pinned!" should be shown
+ And I open data/numbers/2.txt
+ Then the message "Tab is pinned! Opening in new tab." should be shown
And the following tabs should be open:
- data/numbers/1.txt (active) (pinned)
+ - data/numbers/2.txt
Scenario: :tab-pin open url with tabs.pinned.frozen = false
When I set tabs.pinned.frozen to false
diff --git a/tests/end2end/features/test_editor_bdd.py b/tests/end2end/features/test_editor_bdd.py
index 445691bee..40f77a0f7 100644
--- a/tests/end2end/features/test_editor_bdd.py
+++ b/tests/end2end/features/test_editor_bdd.py
@@ -32,7 +32,7 @@ bdd.scenarios('editor.feature')
from qutebrowser.utils import utils
-@bdd.when(bdd.parsers.parse('I set up a fake editor replacing "{text}" by '
+@bdd.when(bdd.parsers.parse('I setup a fake editor replacing "{text}" by '
'"{replacement}"'))
def set_up_editor_replacement(quteproc, server, tmpdir, text, replacement):
"""Set up editor.command to a small python script doing a replacement."""
@@ -53,7 +53,7 @@ def set_up_editor_replacement(quteproc, server, tmpdir, text, replacement):
quteproc.set_setting('editor.command', editor)
-@bdd.when(bdd.parsers.parse('I set up a fake editor returning "{text}"'))
+@bdd.when(bdd.parsers.parse('I setup a fake editor returning "{text}"'))
def set_up_editor(quteproc, tmpdir, text):
"""Set up editor.command to a small python script inserting a text."""
script = tmpdir / 'script.py'
@@ -67,7 +67,7 @@ def set_up_editor(quteproc, tmpdir, text):
quteproc.set_setting('editor.command', editor)
-@bdd.when(bdd.parsers.parse('I set up a fake editor returning empty text'))
+@bdd.when(bdd.parsers.parse('I setup a fake editor returning empty text'))
def set_up_editor_empty(quteproc, tmpdir):
"""Set up editor.command to a small python script inserting empty text."""
set_up_editor(quteproc, tmpdir, "")
@@ -107,7 +107,7 @@ def editor_pid_watcher(tmpdir):
return EditorPidWatcher(tmpdir)
-@bdd.when(bdd.parsers.parse('I set up a fake editor that writes "{text}" on '
+@bdd.when(bdd.parsers.parse('I setup a fake editor that writes "{text}" on '
'save'))
def set_up_editor_wait(quteproc, tmpdir, text, editor_pid_watcher):
"""Set up editor.command to a small python script inserting a text."""
@@ -180,18 +180,31 @@ def save_editor_wait(tmpdir):
os.kill(pid, signal.SIGUSR2)
-@bdd.when(bdd.parsers.parse('I set up a fake "{kind}" fileselector '
- 'selecting "{files}"'))
-def set_up_fileselector(quteproc, py_proc, kind, files):
+@bdd.when(bdd.parsers.parse('I setup a fake {kind} fileselector '
+ 'selecting "{files}" and writes to {output_type}'))
+def set_up_fileselector(quteproc, py_proc, kind, files, output_type):
"""Set up fileselect.xxx.command to select the file(s)."""
cmd, args = py_proc(r"""
import os
import sys
- tmp_file = sys.argv[1]
- with open(tmp_file, 'w') as f:
- for selected_file in sys.argv[2:]:
- f.write(os.path.abspath(selected_file) + "\n")
+ tmp_file = None
+ for i, arg in enumerate(sys.argv):
+ if arg.startswith('--file='):
+ tmp_file = arg[len('--file='):]
+ sys.argv.pop(i)
+ break
+ selected_files = sys.argv[1:]
+ if tmp_file is None:
+ for selected_file in selected_files:
+ print(os.path.abspath(selected_file))
+ else:
+ with open(tmp_file, 'w') as f:
+ for selected_file in selected_files:
+ f.write(os.path.abspath(selected_file) + '\n')
""")
- fileselect_cmd = json.dumps([cmd, *args, '{}', *files.split(' ')])
+ args += files.split(' ')
+ if output_type == "a temporary file":
+ args += ['--file={}']
+ fileselect_cmd = json.dumps([cmd, *args])
quteproc.set_setting('fileselect.handler', 'external')
quteproc.set_setting(f'fileselect.{kind}.command', fileselect_cmd)
diff --git a/tests/unit/browser/webengine/test_webenginetab.py b/tests/unit/browser/webengine/test_webenginetab.py
index 7827c379b..156f7d26f 100644
--- a/tests/unit/browser/webengine/test_webenginetab.py
+++ b/tests/unit/browser/webengine/test_webenginetab.py
@@ -20,6 +20,7 @@
"""Test webenginetab."""
import logging
+import textwrap
import pytest
QtWebEngineWidgets = pytest.importorskip("PyQt5.QtWebEngineWidgets")
@@ -35,15 +36,38 @@ webenginetab = pytest.importorskip(
pytestmark = pytest.mark.usefixtures('greasemonkey_manager')
+class ScriptsHelper:
+
+ """Helper to get the processed (usually Greasemonkey) scripts."""
+
+ def __init__(self, tab):
+ self._tab = tab
+
+ def get_scripts(self, prefix='GM-'):
+ return [
+ s for s in self._tab._widget.page().scripts().toList()
+ if s.name().startswith(prefix)
+ ]
+
+ def get_script(self):
+ scripts = self.get_scripts()
+ assert len(scripts) == 1
+ return scripts[0]
+
+ def inject(self, scripts):
+ self._tab._scripts._inject_greasemonkey_scripts(scripts)
+ return self.get_scripts()
+
+
class TestWebengineScripts:
"""Test the _WebEngineScripts utility class."""
@pytest.fixture
- def webengine_scripts(self, webengine_tab):
- return webengine_tab._scripts
+ def scripts_helper(self, webengine_tab):
+ return ScriptsHelper(webengine_tab)
- def test_greasemonkey_undefined_world(self, webengine_scripts, caplog):
+ def test_greasemonkey_undefined_world(self, scripts_helper, caplog):
"""Make sure scripts with non-existent worlds are rejected."""
scripts = [
greasemonkey.GreasemonkeyScript(
@@ -51,18 +75,16 @@ class TestWebengineScripts:
]
with caplog.at_level(logging.ERROR, 'greasemonkey'):
- webengine_scripts._inject_greasemonkey_scripts(scripts)
+ injected = scripts_helper.inject(scripts)
assert len(caplog.records) == 1
msg = caplog.messages[0]
assert "has invalid value for '@qute-js-world': Mars" in msg
- collection = webengine_scripts._widget.page().scripts().toList()
- assert not any(script.name().startswith('GM-')
- for script in collection)
+
+ assert not injected
@pytest.mark.parametrize("worldid", [-1, 257])
- def test_greasemonkey_out_of_range_world(self, worldid, webengine_scripts,
- caplog):
+ def test_greasemonkey_out_of_range_world(self, worldid, scripts_helper, caplog):
"""Make sure scripts with out-of-range worlds are rejected."""
scripts = [
greasemonkey.GreasemonkeyScript(
@@ -70,19 +92,18 @@ class TestWebengineScripts:
]
with caplog.at_level(logging.ERROR, 'greasemonkey'):
- webengine_scripts._inject_greasemonkey_scripts(scripts)
+ injected = scripts_helper.inject(scripts)
assert len(caplog.records) == 1
msg = caplog.messages[0]
assert "has invalid value for '@qute-js-world': " in msg
assert "should be between 0 and" in msg
- collection = webengine_scripts._widget.page().scripts().toList()
- assert not any(script.name().startswith('GM-')
- for script in collection)
+
+ assert not injected
@pytest.mark.parametrize("worldid", [0, 10])
def test_greasemonkey_good_worlds_are_passed(self, worldid,
- webengine_scripts, caplog):
+ scripts_helper, caplog):
"""Make sure scripts with valid worlds have it set."""
scripts = [
greasemonkey.GreasemonkeyScript(
@@ -91,13 +112,11 @@ class TestWebengineScripts:
]
with caplog.at_level(logging.ERROR, 'greasemonkey'):
- webengine_scripts._inject_greasemonkey_scripts(scripts)
+ scripts_helper.inject(scripts)
- collection = webengine_scripts._widget.page().scripts()
- assert collection.toList()[-1].worldId() == worldid
+ assert scripts_helper.get_script().worldId() == worldid
- def test_greasemonkey_document_end_workaround(self, monkeypatch,
- webengine_scripts):
+ def test_greasemonkey_document_end_workaround(self, monkeypatch, scripts_helper):
"""Make sure document-end is forced when needed."""
monkeypatch.setattr(greasemonkey.objects, 'backend',
usertypes.Backend.QtWebEngine)
@@ -109,13 +128,42 @@ class TestWebengineScripts:
('run-at', 'document-start'),
], None)
]
+ scripts_helper.inject(scripts)
- webengine_scripts._inject_greasemonkey_scripts(scripts)
-
- collection = webengine_scripts._widget.page().scripts()
- script = collection.toList()[-1]
+ script = scripts_helper.get_script()
assert script.injectionPoint() == QWebEngineScript.DocumentReady
+ @pytest.mark.parametrize('run_at, expected', [
+ # UserScript::DocumentElementCreation
+ ('document-start', QWebEngineScript.DocumentCreation),
+ # UserScript::DocumentLoadFinished
+ ('document-end', QWebEngineScript.DocumentReady),
+ # UserScript::AfterLoad
+ ('document-idle', QWebEngineScript.Deferred),
+ # default according to https://wiki.greasespot.net/Metadata_Block#.40run-at
+ (None, QWebEngineScript.DocumentReady),
+ ])
+ def test_greasemonkey_run_at_values(self, scripts_helper, run_at, expected):
+ if run_at is None:
+ script = """
+ // ==UserScript==
+ // @name qutebrowser test userscript
+ // ==/UserScript==
+ """
+ else:
+ script = f"""
+ // ==UserScript==
+ // @name qutebrowser test userscript
+ // @run-at {run_at}
+ // ==/UserScript==
+ """
+
+ script = textwrap.dedent(script.lstrip('\n'))
+ scripts = [greasemonkey.GreasemonkeyScript.parse(script)]
+ scripts_helper.inject(scripts)
+
+ assert scripts_helper.get_script().injectionPoint() == expected
+
def test_notification_permission_workaround():
"""Make sure the value for QWebEnginePage::Notifications is correct."""
diff --git a/tests/unit/completion/test_models.py b/tests/unit/completion/test_models.py
index 8a6b24557..22e9c6490 100644
--- a/tests/unit/completion/test_models.py
+++ b/tests/unit/completion/test_models.py
@@ -28,7 +28,7 @@ from datetime import datetime
from unittest import mock
import hypothesis
-import hypothesis.strategies
+import hypothesis.strategies as hst
import pytest
from PyQt5.QtCore import QUrl, QDateTime
try:
@@ -459,9 +459,10 @@ def test_filesystem_completion_model_interface(info, local_files_path):
@hypothesis.given(
- as_uri=hypothesis.strategies.booleans(),
- add_sep=hypothesis.strategies.booleans(),
- text=hypothesis.strategies.text(),
+ as_uri=hst.booleans(),
+ add_sep=hst.booleans(),
+ text=hst.text(alphabet=hst.characters(
+ blacklist_categories=['Cc'], blacklist_characters='\x00')),
)
def test_filesystem_completion_hypothesis(info, as_uri, add_sep, text):
if as_uri:
@@ -1445,7 +1446,7 @@ def undo_completion_retains_sort_order(tabbed_browser_stubs, info):
_check_completions(model, {"Closed tabs": expected})
-@hypothesis.given(text=hypothesis.strategies.text())
+@hypothesis.given(text=hst.text())
def test_listcategory_hypothesis(text):
"""Make sure we can't produce invalid patterns."""
cat = listcategory.ListCategory("test", [])
diff --git a/tests/unit/javascript/conftest.py b/tests/unit/javascript/conftest.py
index 47884687d..85d5ebe0a 100644
--- a/tests/unit/javascript/conftest.py
+++ b/tests/unit/javascript/conftest.py
@@ -28,6 +28,7 @@ import jinja2
from PyQt5.QtCore import QUrl
import qutebrowser
+from qutebrowser.utils import usertypes
class JSTester:
@@ -113,7 +114,7 @@ class JSTester:
source = f.read()
self.run(source, expected)
- def run(self, source: str, expected, world=None) -> None:
+ def run(self, source: str, expected=usertypes.UNSET, world=None) -> None:
"""Run the given javascript source.
Args:
@@ -123,7 +124,9 @@ class JSTester:
"""
with self.qtbot.wait_callback() as callback:
self.tab.run_js_async(source, callback, world=world)
- callback.assert_called_with(expected)
+
+ if expected is not usertypes.UNSET:
+ callback.assert_called_with(expected)
@pytest.fixture
diff --git a/tests/unit/javascript/test_greasemonkey.py b/tests/unit/javascript/test_greasemonkey.py
index c28b9c8f7..3a3ea0294 100644
--- a/tests/unit/javascript/test_greasemonkey.py
+++ b/tests/unit/javascript/test_greasemonkey.py
@@ -25,7 +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.utils import usertypes, version
from qutebrowser.browser import greasemonkey
from qutebrowser.misc import objects
@@ -77,8 +77,7 @@ def test_get_scripts_by_url(url, expected_matches):
gm_manager = greasemonkey.GreasemonkeyManager()
scripts = gm_manager.scripts_for(QUrl(url))
- assert (len(scripts.start + scripts.end + scripts.idle) ==
- expected_matches)
+ assert len(scripts.start + scripts.end + scripts.idle) == expected_matches
@pytest.mark.parametrize("url, expected_matches", [
@@ -102,8 +101,7 @@ def test_regex_includes_scripts_for(url, expected_matches):
gm_manager = greasemonkey.GreasemonkeyManager()
scripts = gm_manager.scripts_for(QUrl(url))
- assert (len(scripts.start + scripts.end + scripts.idle) ==
- expected_matches)
+ assert len(scripts.start + scripts.end + scripts.idle) == expected_matches
def test_no_metadata(caplog):
@@ -229,124 +227,87 @@ def test_required_scripts_are_included(download_stub, tmpdir):
assert scripts[0].excludes
-class TestWindowIsolation:
+def test_window_isolation(js_tester, request):
"""Check that greasemonkey scripts get a shadowed global scope."""
+ # Change something in the global scope
+ setup_script = "window.$ = 'global'"
- @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([
- """
- const result = [];
- """,
- test_script.code(),
- """
- // Now check that the actual global scope has
- // not been overwritten
+ # Greasemonkey script to report back on its scope.
+ test_gm_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($);
- // And return our findings
- result;
- """
- ])
+ """)
+ )
+
+ # The compiled source of that scripts with some additional setup
+ # bookending it.
+ test_script = "\n".join([
+ """
+ const result = [];
+ """,
+ test_gm_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
+ # What we expect the script to report back.
+ expected = ["global", "global", "shadowed", "shadowed", "global", "global"]
- def test_webengine(self, qtbot, webengineview, setup):
- page = webengineview.page()
- page.runJavaScript(setup.setup_script)
+ # The JSCore in 602.1 doesn't fully support Proxy.
+ xfail = False
+ if (js_tester.tab.backend == usertypes.Backend.QtWebKit and
+ version.qWebKitVersion() == '602.1'):
+ expected[-1] = 'shadowed'
+ expected[-2] = 'shadowed'
+ xfail = True
- with qtbot.wait_callback() as callback:
- page.runJavaScript(setup.test_script, callback)
- callback.assert_called_with(setup.expected)
+ js_tester.run(setup_script)
+ js_tester.run(test_script, expected=expected)
- # The JSCore in 602.1 doesn't fully support Proxy.
- @pytest.mark.qtwebkit6021_xfail
- 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
+ if xfail:
+ pytest.xfail("Broken on WebKit 602.1")
-class TestSharedWindowProxy:
+def test_shared_window_proxy(js_tester):
"""Check that all scripts have access to the same window proxy."""
+ # Greasemonkey script to add a property to the window proxy.
+ test_script_a = greasemonkey.GreasemonkeyScript.parse(
+ textwrap.dedent("""
+ // ==UserScript==
+ // @name a
+ // ==/UserScript==
+ // Set a value from script a
+ window.$ = 'test';
+ """)
+ ).code()
+
+ # Greasemonkey script to retrieve a property from the window proxy.
+ test_script_b = greasemonkey.GreasemonkeyScript.parse(
+ textwrap.dedent("""
+ // ==UserScript==
+ // @name b
+ // ==/UserScript==
+ // Check that the value is accessible from script b
+ return [window.$, $];
+ """)
+ ).code()
- @pytest.fixture
- def setup(self):
- # pylint: disable=attribute-defined-outside-init
- class SetupData:
- pass
- ret = SetupData()
-
- # Greasemonkey script to add a property to the window proxy.
- ret.test_script_a = greasemonkey.GreasemonkeyScript.parse(
- textwrap.dedent("""
- // ==UserScript==
- // @name a
- // ==/UserScript==
- // Set a value from script a
- window.$ = 'test';
- """)
- ).code()
-
- # Greasemonkey script to retrieve a property from the window proxy.
- ret.test_script_b = greasemonkey.GreasemonkeyScript.parse(
- textwrap.dedent("""
- // ==UserScript==
- // @name b
- // ==/UserScript==
- // Check that the value is accessible from script b
- return [window.$, $];
- """)
- ).code()
-
- # What we expect the script to report back.
- ret.expected = ["test", "test"]
- return ret
-
- def test_webengine(self, qtbot, webengineview, setup):
- page = webengineview.page()
-
- with qtbot.wait_callback() as callback:
- page.runJavaScript(setup.test_script_a, callback)
- with qtbot.wait_callback() as callback:
- page.runJavaScript(setup.test_script_b, callback)
- callback.assert_called_with(setup.expected)
-
- def test_webkit(self, webview, setup):
- elem = webview.page().mainFrame().documentElement()
- elem.evaluateJavaScript(setup.test_script_a)
- result = elem.evaluateJavaScript(setup.test_script_b)
- assert result == setup.expected
+ js_tester.run(test_script_a)
+ js_tester.run(test_script_b, expected=["test", "test"])
diff --git a/tests/unit/misc/test_guiprocess.py b/tests/unit/misc/test_guiprocess.py
index a2acad1ac..9e1b3916c 100644
--- a/tests/unit/misc/test_guiprocess.py
+++ b/tests/unit/misc/test_guiprocess.py
@@ -127,9 +127,11 @@ def test_start_output_message(proc, qtbot, caplog, message_mock, py_proc,
if stdout_msg is not None:
assert stdout_msg.level == usertypes.MessageLevel.info
assert stdout_msg.text == 'stdout text'
+ assert proc.final_stdout.strip() == "stdout text", proc.final_stdout
if stderr_msg is not None:
assert stderr_msg.level == usertypes.MessageLevel.error
assert stderr_msg.text == 'stderr text'
+ assert proc.final_stderr.strip() == "stderr text", proc.final_stderr
def test_start_env(monkeypatch, qtbot, py_proc):