Compare commits
117 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
718f73be2e | ||
|
|
da4865d408 | ||
|
|
fd4ff3c9ce | ||
|
|
ad9b50601c | ||
|
|
c88a94f1cc | ||
|
|
13332cd2cf | ||
|
|
48808e59cb | ||
|
|
5571ef4e62 | ||
|
|
b4de889df9 | ||
|
|
4c9360237f | ||
|
|
10538738e0 | ||
|
|
bd5f84bddf | ||
|
|
93ae6ad592 | ||
|
|
146f8e72ae | ||
|
|
f61662fa52 | ||
|
|
e9b4c2a66e | ||
|
|
6a7ab7edb3 | ||
|
|
f7f96484e8 | ||
|
|
840d2e4423 | ||
|
|
e54f2a090a | ||
|
|
55ce4b7ed2 | ||
|
|
3daf823da8 | ||
|
|
72feb2c19f | ||
|
|
6d04508490 | ||
|
|
93ebd846ab | ||
|
|
9ee473a54c | ||
|
|
d1cced0da4 | ||
|
|
22644c41da | ||
|
|
6b5857ef7d | ||
|
|
b0f4cc6924 | ||
|
|
218637af84 | ||
|
|
dd84845f01 | ||
|
|
cf53f9042a | ||
|
|
f48266f72f | ||
|
|
5dac848968 | ||
|
|
341aa1e700 | ||
|
|
ebf81c06ae | ||
|
|
a3eb8d6561 | ||
|
|
af4b02bf46 | ||
|
|
ac29c579ff | ||
|
|
db5ec363cd | ||
|
|
f352c72d1d | ||
|
|
b1f1a0cafa | ||
|
|
749056ff90 | ||
|
|
e7b00ace73 | ||
|
|
442bdd4a4f | ||
|
|
900efe4a36 | ||
|
|
f8a78a0962 | ||
|
|
60e8abaa89 | ||
|
|
7762017f00 | ||
|
|
2ba40acf74 | ||
|
|
388c155ebb | ||
|
|
adf2f9860d | ||
|
|
f528a5dd21 | ||
|
|
f0f1a4a1d1 | ||
|
|
ae295a7f65 | ||
|
|
68794cc2e2 | ||
|
|
acdf0a1c60 | ||
|
|
626abd3c83 | ||
|
|
979b7cfaba | ||
|
|
2b6b4e82a7 | ||
|
|
80778a9ff3 | ||
|
|
6eb8284fe0 | ||
|
|
49bdcd5a97 | ||
|
|
106e591a36 | ||
|
|
a70f864ff5 | ||
|
|
4932cc4d24 | ||
|
|
2b5f133726 | ||
|
|
5436e27e41 | ||
|
|
4adf10a2f2 | ||
|
|
b80fa7a197 | ||
|
|
3cc790afb3 | ||
|
|
91aa9f6c0c | ||
|
|
d6cacdb42f | ||
|
|
19554ba4a1 | ||
|
|
d16d9e403a | ||
|
|
432d666d25 | ||
|
|
1c3ee0db20 | ||
|
|
bdc0c0ddc1 | ||
|
|
b9c8a79f10 | ||
|
|
801e9e0334 | ||
|
|
cfa5ee2835 | ||
|
|
9c6437b3b9 | ||
|
|
486488e2cd | ||
|
|
92aedf84f5 | ||
|
|
6825dfb8d8 | ||
|
|
d6c6014b85 | ||
|
|
c1ac1d702f | ||
|
|
9e50b7afcc | ||
|
|
1388880e7b | ||
|
|
30d60ea740 | ||
|
|
28cac01a1f | ||
|
|
1689cb09f8 | ||
|
|
286c71a48a | ||
|
|
c073234a8d | ||
|
|
8c286412cb | ||
|
|
b3cef948b0 | ||
|
|
c94ea5f8d4 | ||
|
|
e2d249541d | ||
|
|
2de6428830 | ||
|
|
c2472d88f1 | ||
|
|
1d2dd5bf55 | ||
|
|
cbb246fd0b | ||
|
|
646e92707a | ||
|
|
c5334fb683 | ||
|
|
76dbfa7305 | ||
|
|
9ad6cef369 | ||
|
|
423192e9c9 | ||
|
|
d606cd5550 | ||
|
|
7c1de99876 | ||
|
|
b873cfb18a | ||
|
|
a1776087e0 | ||
|
|
948866f4f2 | ||
|
|
a9a7f5da45 | ||
|
|
b169a1c802 | ||
|
|
f9e702bae5 | ||
|
|
29eadf7141 |
2
.flake8
2
.flake8
@@ -32,7 +32,7 @@ exclude = .*,__pycache__,resources.py
|
||||
# D403: First word of the first line should be properly capitalized
|
||||
# (false-positives)
|
||||
# D413: Missing blank line after last section (not in pep257?)
|
||||
# A003: Builtin name for class attribute (needed for attrs)
|
||||
# A003: Builtin name for class attribute (needed for overridden methods)
|
||||
ignore =
|
||||
B001,B008,B305,
|
||||
E128,E226,E265,E501,E402,E266,E722,E731,
|
||||
|
||||
@@ -15,8 +15,58 @@ breaking changes (such as renamed commands) can happen in minor releases.
|
||||
// `Fixed` for any bug fixes.
|
||||
// `Security` to invite users to upgrade in case of vulnerabilities.
|
||||
|
||||
v1.3.0 (unreleased)
|
||||
-------------------
|
||||
v1.3.3
|
||||
------
|
||||
|
||||
Security
|
||||
~~~~~~~~
|
||||
|
||||
- An XSS vulnerability on the `qute://history` page allowed websites to inject
|
||||
HTML into the page via a crafted title tag. This could allow them to steal
|
||||
your browsing history. If you're currently unable to upgrade, avoid using
|
||||
`:history`. A CVE request for this issue is pending, see
|
||||
https://github.com/qutebrowser/qutebrowser/issues/4011[#4011] for updates.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Crash in a workaround for a Qt 5.11 bug in rare circumstances.
|
||||
- Workaround for a Qt bug which preserves searches between page loads.
|
||||
- In v1.3.2 a dependency on the `PyQt5.QtQuickWidgets` module was accidentally
|
||||
introduced. Since that module isn't packaged everywhere, it's been removed
|
||||
again.
|
||||
|
||||
v1.3.2
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- QtWebEngine: Improved workaround for a bug in Qt 5.11 where only the
|
||||
top/bottom half of the window is used.
|
||||
- QtWebEngine: Work around a bug in Qt 5.11 where an endless loading-loop is
|
||||
triggered when clicking a link with an unknown scheme.
|
||||
- QtWebEngine: When switching between pages with changed settings, less
|
||||
unnecessary reloads are done now.
|
||||
- QtWebEngine: It's now possible to open external links such as `magnet://` or
|
||||
`mailto:` via hints.
|
||||
|
||||
v1.3.1
|
||||
------
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Work around a bug in Qt 5.11 where only the top/bottom half of the window is used.
|
||||
This workaround is incomplete, but fixes the majority of the cases where this happens.
|
||||
- Work around keyboard focus issues with Qt 5.11.
|
||||
- Work around an issue in Qt 5.11 where e.g. activating JavaScript per-domain
|
||||
needed a manual reload in some cases.
|
||||
- Don't crash when a ² key is pressed (e.g. on AZERTY keyboards).
|
||||
- Don't crash when a tab is opened and quickly closed again.
|
||||
|
||||
v1.3.0
|
||||
------
|
||||
|
||||
Added
|
||||
~~~~~
|
||||
@@ -25,7 +75,9 @@ Added
|
||||
- New `url.open_base_url` option to open the base URL of a searchengine when no
|
||||
search term is given.
|
||||
- New `tabs.min_width` setting to configure the minimal width for tabs.
|
||||
- New `getbib` userscript to download bibtex information for DOIs on a page.
|
||||
- New userscripts:
|
||||
* `getbib` to download bibtex information for DOIs on a page.
|
||||
* `qute-keepass` to get passwords from KeePassX.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
@@ -52,7 +104,6 @@ Changed
|
||||
- Error messages when trying to wrap when `tabs.wrap` is `False` are now logged
|
||||
to debug instead of messages.
|
||||
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
@@ -81,7 +132,18 @@ Fixed
|
||||
- The Makefile (intended for packagers) now supports `PREFIX` properly.
|
||||
- The workaround for a black window with Nvidia graphics is now enabled on
|
||||
non-Linux systems (like FreeBSD) as well.
|
||||
- Initial support for Qt 5.11
|
||||
- Initial support for Qt 5.11.
|
||||
- Checking for a new version after sending a crash report now works properly
|
||||
again.
|
||||
- `@match` in Greasemonkey scripts now more closely matches the proper pattern
|
||||
syntax.
|
||||
- Searching via `/` or `?` now doesn't handle any characters in a special way.
|
||||
- Fixed crash when trying to retry some failed downloads on QtWebEngine.
|
||||
- An invalid spellcheck dictionary filename now doesn't crash anymore.
|
||||
- When no spellcheck dictionaries are configured, it's now disabled internally.
|
||||
This works around an issue with entering special characters on Facebook
|
||||
messenger.
|
||||
- The macOS release now should work again on macOS 10.11 and newer.
|
||||
|
||||
v1.2.1
|
||||
------
|
||||
|
||||
@@ -3,44 +3,28 @@ Configuring qutebrowser
|
||||
|
||||
IMPORTANT: qutebrowser's configuration system was completely rewritten in
|
||||
September 2017. This information is not applicable to older releases, and older
|
||||
information elsewhere might be outdated. **If you had an old configuration
|
||||
around and upgraded, this page will automatically open once**. To view it at a
|
||||
later time, use the `:help` command.
|
||||
information elsewhere might be outdated.
|
||||
|
||||
Migrating older configurations
|
||||
------------------------------
|
||||
qutebrowser's config files
|
||||
--------------------------
|
||||
|
||||
qutebrowser does no automatic migration for the new configuration. However,
|
||||
there's a special link:qute://configdiff/old[configdiff] page
|
||||
(`qute://configdiff/old`) in qutebrowser, which will show you the changes you
|
||||
did in your old configuration, compared to the old defaults.
|
||||
qutebrowser releases before v1.0.0 had a `qutebrowser.conf` and `keys.conf`
|
||||
file. Those are not used anymore since that release - see
|
||||
<<migrating,"Migrating older configurations">> for information on how to
|
||||
migrate to the new config.
|
||||
|
||||
Other changes in default settings:
|
||||
When using `:set` and `:bind`, changes are saved to an `autoconfig.yml` file
|
||||
automatically. If you don't want to have a config file which is curated by
|
||||
hand, you can simply use those - see
|
||||
<<autoconfig,"Configuring qutebrowser via the user interface">> for details.
|
||||
|
||||
- In v1.1.x and newer, `<Up>` and `<Down>` navigate through command history
|
||||
if no text was entered yet.
|
||||
With v1.0.x, they always navigate through command history instead of selecting
|
||||
completion items. Use `<Tab>`/`<Shift-Tab>` to cycle through the completion
|
||||
instead.
|
||||
You can get back the old behavior by doing:
|
||||
+
|
||||
----
|
||||
:bind -m command <Up> completion-item-focus prev
|
||||
:bind -m command <Down> completion-item-focus next
|
||||
----
|
||||
+
|
||||
or always navigate through command history with
|
||||
+
|
||||
----
|
||||
:bind -m command <Up> command-history-prev
|
||||
:bind -m command <Down> command-history-next
|
||||
----
|
||||
|
||||
- The default for `completion.web_history_max_items` is now set to `-1`, showing
|
||||
an unlimited number of items in the completion for `:open` as the new
|
||||
sqlite-based completion is much faster. If the `:open` completion is too slow
|
||||
on your machine, set an appropriate limit again.
|
||||
For more advanced configuration, you can write a `config.py` file - see
|
||||
<<configpy,"Configuring qutebrowser via config.py">>. As soon as a `config.py`
|
||||
exists, the `autoconfig.yml` file **is not read anymore** by default. You need
|
||||
to <<configpy-autoconfig,load it by hand>> if you want settings done via
|
||||
`:set`/`:bind` to still persist.
|
||||
|
||||
[[autoconfig]]
|
||||
Configuring qutebrowser via the user interface
|
||||
----------------------------------------------
|
||||
|
||||
@@ -88,6 +72,7 @@ link:commands.html#config-clear[`:config-clear`] to reset the entire configurati
|
||||
and link:commands.html#config-cycle[`:config-cycle`] to cycle a setting between
|
||||
different values.
|
||||
|
||||
[[configpy]]
|
||||
Configuring qutebrowser via config.py
|
||||
-------------------------------------
|
||||
|
||||
@@ -239,6 +224,7 @@ config.bind(',v', 'spawn mpv {url}')
|
||||
To suppress loading of any default keybindings, you can set
|
||||
`c.bindings.default = {}`.
|
||||
|
||||
[[configpy-autoconfig]]
|
||||
Loading `autoconfig.yml`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -429,3 +415,38 @@ from qutebrowser.config.config import ConfigContainer # noqa: F401
|
||||
config = config # type: ConfigAPI # noqa: F821 pylint: disable=E0602,C0103
|
||||
c = c # type: ConfigContainer # noqa: F821 pylint: disable=E0602,C0103
|
||||
----
|
||||
|
||||
[[migrating]]
|
||||
Migrating older configurations
|
||||
------------------------------
|
||||
|
||||
qutebrowser does no automatic migration for the new configuration. However,
|
||||
there's a special link:qute://configdiff/old[configdiff] page
|
||||
(`qute://configdiff/old`) in qutebrowser, which will show you the changes you
|
||||
did in your old configuration, compared to the old defaults.
|
||||
|
||||
Other changes in default settings:
|
||||
|
||||
- In v1.1.x and newer, `<Up>` and `<Down>` navigate through command history
|
||||
if no text was entered yet.
|
||||
With v1.0.x, they always navigate through command history instead of selecting
|
||||
completion items. Use `<Tab>`/`<Shift-Tab>` to cycle through the completion
|
||||
instead.
|
||||
You can get back the old behavior by doing:
|
||||
+
|
||||
----
|
||||
:bind -m command <Up> completion-item-focus prev
|
||||
:bind -m command <Down> completion-item-focus next
|
||||
----
|
||||
+
|
||||
or always navigate through command history with
|
||||
+
|
||||
----
|
||||
:bind -m command <Up> command-history-prev
|
||||
:bind -m command <Down> command-history-next
|
||||
----
|
||||
|
||||
- The default for `completion.web_history_max_items` is now set to `-1`, showing
|
||||
an unlimited number of items in the completion for `:open` as the new
|
||||
sqlite-based completion is much faster. If the `:open` completion is too slow
|
||||
on your machine, set an appropriate limit again.
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
check-manifest==0.36
|
||||
check-manifest==0.37
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
certifi==2018.1.18
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
codecov==2.0.15
|
||||
coverage==4.5.1
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
attrs==17.4.0
|
||||
flake8==3.5.0
|
||||
flake8-bugbear==18.2.0
|
||||
flake8-builtins==1.2.2
|
||||
flake8-builtins==1.3.1
|
||||
flake8-comprehensions==1.4.1
|
||||
flake8-copyright==0.2.0
|
||||
flake8-debugger==3.1.0
|
||||
@@ -19,7 +19,7 @@ flake8-tuple==0.2.13
|
||||
mccabe==0.6.1
|
||||
pathmatch==0.2.1
|
||||
pep8-naming==0.5.0
|
||||
pycodestyle==2.3.1
|
||||
pycodestyle==2.3.1 # rq.filter: < 2.4.0
|
||||
pydocstyle==2.1.1
|
||||
pyflakes==1.6.0
|
||||
six==1.11.0
|
||||
|
||||
@@ -15,3 +15,6 @@ flake8-tuple
|
||||
pep8-naming
|
||||
pydocstyle
|
||||
pyflakes
|
||||
|
||||
# https://github.com/PyCQA/pycodestyle/issues/741
|
||||
#@ filter: pycodestyle < 2.4.0
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
appdirs==1.4.3
|
||||
packaging==17.1
|
||||
pyparsing==2.2.0
|
||||
setuptools==39.0.1
|
||||
setuptools==39.1.0
|
||||
six==1.11.0
|
||||
wheel==0.31.0
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
certifi==2018.1.18
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
github3.py==1.0.2
|
||||
github3.py==1.1.0
|
||||
idna==2.6
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
astroid==1.6.3
|
||||
certifi==2018.1.18
|
||||
certifi==2018.4.16
|
||||
chardet==3.0.4
|
||||
github3.py==1.0.2
|
||||
github3.py==1.1.0
|
||||
idna==2.6
|
||||
isort==4.3.4
|
||||
lazy-object-proxy==1.3.1
|
||||
|
||||
@@ -2,16 +2,16 @@
|
||||
|
||||
attrs==17.4.0
|
||||
beautifulsoup4==4.6.0
|
||||
cheroot==6.1.2
|
||||
cheroot==6.2.4
|
||||
click==6.7
|
||||
# colorama==0.3.9
|
||||
coverage==4.5.1
|
||||
EasyProcess==0.2.3
|
||||
fields==5.0.0
|
||||
Flask==0.12.2
|
||||
Flask==1.0.1
|
||||
glob2==0.6
|
||||
hunter==2.0.2
|
||||
hypothesis==3.55.1
|
||||
hypothesis==3.56.5
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.10
|
||||
Mako==1.0.7
|
||||
@@ -22,13 +22,13 @@ parse-type==0.4.2
|
||||
pluggy==0.6.0
|
||||
py==1.5.3
|
||||
py-cpuinfo==4.0.0
|
||||
pytest==3.5.0
|
||||
pytest==3.5.1
|
||||
pytest-bdd==2.21.0
|
||||
pytest-benchmark==3.1.1
|
||||
pytest-cov==2.5.1
|
||||
pytest-faulthandler==1.5.0
|
||||
pytest-instafail==0.3.0
|
||||
pytest-mock==1.8.0
|
||||
pytest-mock==1.9.0
|
||||
pytest-qt==2.3.1
|
||||
pytest-repeat==0.4.1
|
||||
pytest-rerunfailures==4.0
|
||||
|
||||
261
misc/userscripts/qute-keepass
Executable file
261
misc/userscripts/qute-keepass
Executable file
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright 2018 Jay Kamat <jaygkamat@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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""This userscript allows for insertion of usernames and passwords from keepass
|
||||
databases using pykeepass. Since it is a userscript, it must be run from
|
||||
qutebrowser.
|
||||
|
||||
A sample invocation of this script is:
|
||||
|
||||
:spawn --userscript qute-keepass -p ~/KeePassFiles/MainDatabase.kdbx
|
||||
|
||||
And a sample binding
|
||||
|
||||
:bind --mode=insert <ctrl-i> spawn --userscript qute-keepass -p ~/KeePassFiles/MainDatabase.kdbx
|
||||
|
||||
-p or --path is a required argument.
|
||||
|
||||
--keyfile-path allows you to specify a keepass keyfile. If you only use a
|
||||
keyfile, also add --no-password as well. Specifying --no-password without
|
||||
--keyfile-path will lead to an error.
|
||||
|
||||
login information is inserted using :insert-text and :fake-key <Tab>, which
|
||||
means you must have a cursor in position before initiating this userscript. If
|
||||
you do not do this, you will get 'element not editable' errors.
|
||||
|
||||
If keepass takes a while to open the DB, you might want to consider reducing
|
||||
the number of transform rounds in your database settings.
|
||||
|
||||
Dependencies: pykeepass (in python3), PyQt5. Without pykeepass, you will get an
|
||||
exit code of 100.
|
||||
|
||||
********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************
|
||||
|
||||
WARNING: The login details are viewable as plaintext in qutebrowser's debug log
|
||||
(qute://log) and could be compromised if you decide to submit a crash report!
|
||||
|
||||
********************!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!******************
|
||||
|
||||
"""
|
||||
|
||||
# pylint: disable=bad-builtin
|
||||
|
||||
import argparse
|
||||
import enum
|
||||
import functools
|
||||
import os
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtWidgets import QApplication, QInputDialog, QLineEdit
|
||||
|
||||
try:
|
||||
import pykeepass
|
||||
except ImportError as e:
|
||||
print("pykeepass not found: {}".format(str(e)), file=sys.stderr)
|
||||
|
||||
# Since this is a common error, try to print it to the FIFO if we can.
|
||||
if 'QUTE_FIFO' in os.environ:
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
|
||||
fifo.write('message-error "pykeepass failed to be imported."\n')
|
||||
fifo.flush()
|
||||
sys.exit(100)
|
||||
|
||||
argument_parser = argparse.ArgumentParser(
|
||||
description="Fill passwords using keepass.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog=__doc__)
|
||||
argument_parser.add_argument('url', nargs='?', default=os.getenv('QUTE_URL'))
|
||||
argument_parser.add_argument('--path', '-p', required=True,
|
||||
help='Path to the keepass db.')
|
||||
argument_parser.add_argument('--keyfile-path', '-k', default=None,
|
||||
help='Path to a keepass keyfile')
|
||||
argument_parser.add_argument(
|
||||
'--no-password', action='store_true',
|
||||
help='Supply if no password is required to unlock this database. '
|
||||
'Only allowed with --keyfile-path')
|
||||
argument_parser.add_argument(
|
||||
'--dmenu-invocation', '-d', default='dmenu',
|
||||
help='Invocation used to execute a dmenu-provider')
|
||||
argument_parser.add_argument(
|
||||
'--dmenu-format', '-f', default='{title}: {username}',
|
||||
help='Format string for keys to display in dmenu.'
|
||||
' Must generate a unique string.')
|
||||
argument_parser.add_argument(
|
||||
'--no-insert-mode', '-n', dest='insert_mode', action='store_false',
|
||||
help="Don't automatically enter insert mode")
|
||||
argument_parser.add_argument(
|
||||
'--io-encoding', '-i', default='UTF-8',
|
||||
help='Encoding used to communicate with subprocesses')
|
||||
group = argument_parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--username-fill-only', '-e',
|
||||
action='store_true', help='Only insert username')
|
||||
group.add_argument('--password-fill-only', '-w',
|
||||
action='store_true', help='Only insert password')
|
||||
|
||||
CMD_DELAY = 50
|
||||
|
||||
|
||||
class ExitCodes(enum.IntEnum):
|
||||
"""Stores various exit codes groups to use."""
|
||||
SUCCESS = 0
|
||||
FAILURE = 1
|
||||
# 1 is automatically used if Python throws an exception
|
||||
NO_CANDIDATES = 2
|
||||
USER_QUIT = 3
|
||||
DB_OPEN_FAIL = 4
|
||||
|
||||
INTERNAL_ERROR = 10
|
||||
|
||||
|
||||
def qute_command(command):
|
||||
with open(os.environ['QUTE_FIFO'], 'w') as fifo:
|
||||
fifo.write(command + '\n')
|
||||
fifo.flush()
|
||||
|
||||
|
||||
def stderr(to_print):
|
||||
"""Extra functionality to echo out errors to qb ui."""
|
||||
print(to_print, file=sys.stderr)
|
||||
qute_command('message-error "{}"'.format(to_print))
|
||||
|
||||
|
||||
def dmenu(items, invocation, encoding):
|
||||
"""Runs dmenu with given arguments."""
|
||||
command = shlex.split(invocation)
|
||||
process = subprocess.run(command, input='\n'.join(items).encode(encoding),
|
||||
stdout=subprocess.PIPE)
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def get_password():
|
||||
"""Get a keepass db password from user."""
|
||||
_app = QApplication(sys.argv)
|
||||
text, ok = QInputDialog.getText(
|
||||
None, "KeePass DB Password",
|
||||
"Please enter your KeePass Master Password",
|
||||
QLineEdit.Password)
|
||||
if not ok:
|
||||
stderr('Password Prompt Rejected.')
|
||||
sys.exit(ExitCodes.USER_QUIT)
|
||||
return text
|
||||
|
||||
|
||||
def find_candidates(args, host):
|
||||
"""Finds candidates that match host"""
|
||||
file_path = os.path.expanduser(args.path)
|
||||
|
||||
# TODO find a way to keep the db open, so we don't open (and query
|
||||
# password) it every time
|
||||
|
||||
pw = None
|
||||
if not args.no_password:
|
||||
pw = get_password()
|
||||
|
||||
kf = args.keyfile_path
|
||||
if kf:
|
||||
kf = os.path.expanduser(kf)
|
||||
|
||||
try:
|
||||
kp = pykeepass.PyKeePass(file_path, password=pw, keyfile=kf)
|
||||
except Exception as e:
|
||||
stderr("There was an error opening the DB: {}".format(str(e)))
|
||||
|
||||
return kp.find_entries(url="{}{}{}".format(".*", host, ".*"), regex=True)
|
||||
|
||||
|
||||
def candidate_to_str(args, candidate):
|
||||
"""Turns candidate into a human readable string for dmenu"""
|
||||
return args.dmenu_format.format(title=candidate.title,
|
||||
url=candidate.url,
|
||||
username=candidate.username,
|
||||
path=candidate.path,
|
||||
uuid=candidate.uuid)
|
||||
|
||||
|
||||
def candidate_to_secret(candidate):
|
||||
"""Turns candidate into a generic (user, password) tuple"""
|
||||
return (candidate.username, candidate.password)
|
||||
|
||||
|
||||
def run(args):
|
||||
"""Runs qute-keepass"""
|
||||
if not args.url:
|
||||
argument_parser.print_help()
|
||||
return ExitCodes.FAILURE
|
||||
|
||||
url_host = QUrl(args.url).host()
|
||||
|
||||
if not url_host:
|
||||
stderr('{} was not parsed as a valid URL!'.format(args.url))
|
||||
return ExitCodes.INTERNAL_ERROR
|
||||
|
||||
# Find candidates matching the host of the given URL
|
||||
candidates = find_candidates(args, url_host)
|
||||
if not candidates:
|
||||
stderr('No candidates for URL {!r} found!'.format(args.url))
|
||||
return ExitCodes.NO_CANDIDATES
|
||||
|
||||
# Create a map so we can get turn the resulting string from dmenu back into
|
||||
# a candidate
|
||||
candidates_strs = list(map(functools.partial(candidate_to_str, args),
|
||||
candidates))
|
||||
candidates_map = dict(zip(candidates_strs, candidates))
|
||||
|
||||
if len(candidates) == 1:
|
||||
selection = candidates.pop()
|
||||
else:
|
||||
selection = dmenu(candidates_strs,
|
||||
args.dmenu_invocation,
|
||||
args.io_encoding)
|
||||
|
||||
if selection not in candidates_map:
|
||||
stderr("'{}' was not a valid entry!").format(selection)
|
||||
return ExitCodes.USER_QUIT
|
||||
|
||||
selection = candidates_map[selection]
|
||||
|
||||
username, password = candidate_to_secret(selection)
|
||||
|
||||
insert_mode = ';; enter-mode insert' if args.insert_mode else ''
|
||||
if args.username_fill_only:
|
||||
qute_command('insert-text {}{}'.format(username, insert_mode))
|
||||
elif args.password_fill_only:
|
||||
qute_command('insert-text {}{}'.format(password, insert_mode))
|
||||
else:
|
||||
# Enter username and password using insert-key and fake-key <Tab>
|
||||
# (which supports more passwords than fake-key only), then switch back
|
||||
# into insert-mode, so the form can be directly submitted by hitting
|
||||
# enter afterwards. It dosen't matter when we go into insert mode, but
|
||||
# the other commands need to be be executed sequentially, so we add
|
||||
# delays with later.
|
||||
qute_command('insert-text {} ;;'
|
||||
'later {} fake-key <Tab> ;;'
|
||||
'later {} insert-text {}{}'
|
||||
.format(username, CMD_DELAY,
|
||||
CMD_DELAY * 2, password, insert_mode))
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
arguments = argument_parser.parse_args()
|
||||
sys.exit(run(arguments))
|
||||
@@ -109,6 +109,13 @@ def dmenu(items, invocation, encoding):
|
||||
return process.stdout.decode(encoding).strip()
|
||||
|
||||
|
||||
def fake_key_raw(text):
|
||||
for character in text:
|
||||
# Escape all characters by default, space requires special handling
|
||||
sequence = '" "' if character == ' ' else '\{}'.format(character)
|
||||
qute_command('fake-key {}'.format(sequence))
|
||||
|
||||
|
||||
def main(arguments):
|
||||
if not arguments.url:
|
||||
argument_parser.print_help()
|
||||
@@ -158,15 +165,19 @@ def main(arguments):
|
||||
return ExitCodes.COULD_NOT_MATCH_PASSWORD
|
||||
password = match.group(1)
|
||||
|
||||
insert_mode = ';; enter-mode insert' if arguments.insert_mode else ''
|
||||
if arguments.username_only:
|
||||
qute_command('fake-key {}{}'.format(username, insert_mode))
|
||||
fake_key_raw(username)
|
||||
elif arguments.password_only:
|
||||
qute_command('fake-key {}{}'.format(password, insert_mode))
|
||||
fake_key_raw(password)
|
||||
else:
|
||||
# Enter username and password using fake-key and <Tab> (which seems to work almost universally), then switch
|
||||
# back into insert-mode, so the form can be directly submitted by hitting enter afterwards
|
||||
qute_command('fake-key {} ;; fake-key <Tab> ;; fake-key {}{}'.format(username, password, insert_mode))
|
||||
fake_key_raw(username)
|
||||
qute_command('fake-key <Tab>')
|
||||
fake_key_raw(password)
|
||||
|
||||
if arguments.insert_mode:
|
||||
qute_command('enter-mode insert')
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2018 Florian Bruhin (The Compiler)"
|
||||
__license__ = "GPL"
|
||||
__maintainer__ = __author__
|
||||
__email__ = "mail@qutebrowser.org"
|
||||
__version_info__ = (1, 2, 1)
|
||||
__version_info__ = (1, 3, 3)
|
||||
__version__ = '.'.join(str(e) for e in __version_info__)
|
||||
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."
|
||||
|
||||
|
||||
@@ -724,7 +724,13 @@ class AbstractTab(QWidget):
|
||||
if getattr(evt, 'posted', False):
|
||||
raise utils.Unreachable("Can't re-use an event which was already "
|
||||
"posted!")
|
||||
|
||||
recipient = self.event_target()
|
||||
if recipient is None:
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/3888
|
||||
log.webview.warning("Unable to find event target!")
|
||||
return
|
||||
|
||||
evt.posted = True
|
||||
QApplication.postEvent(recipient, evt)
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ import attr
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
|
||||
|
||||
from qutebrowser.utils import (log, standarddir, jinja, objreg, utils,
|
||||
javascript)
|
||||
javascript, urlmatch)
|
||||
from qutebrowser.commands import cmdutils
|
||||
from qutebrowser.browser import downloads
|
||||
|
||||
@@ -48,6 +48,7 @@ class GreasemonkeyScript:
|
||||
def __init__(self, properties, code):
|
||||
self._code = code
|
||||
self.includes = []
|
||||
self.matches = []
|
||||
self.excludes = []
|
||||
self.requires = []
|
||||
self.description = None
|
||||
@@ -63,8 +64,10 @@ class GreasemonkeyScript:
|
||||
self.namespace = value
|
||||
elif name == 'description':
|
||||
self.description = value
|
||||
elif name in ['include', 'match']:
|
||||
elif name == 'include':
|
||||
self.includes.append(value)
|
||||
elif name == 'match':
|
||||
self.matches.append(value)
|
||||
elif name in ['exclude', 'exclude_match']:
|
||||
self.excludes.append(value)
|
||||
elif name == 'run-at':
|
||||
@@ -92,7 +95,7 @@ class GreasemonkeyScript:
|
||||
props = ""
|
||||
script = cls(re.findall(cls.PROPS_REGEX, props), source)
|
||||
script.script_meta = props
|
||||
if not script.includes:
|
||||
if not script.includes and not script.matches:
|
||||
script.includes = ['*']
|
||||
return script
|
||||
|
||||
@@ -117,7 +120,7 @@ class GreasemonkeyScript:
|
||||
return json.dumps({
|
||||
'name': self.name,
|
||||
'description': self.description,
|
||||
'matches': self.includes,
|
||||
'matches': self.matches,
|
||||
'includes': self.includes,
|
||||
'excludes': self.excludes,
|
||||
'run-at': self.run_at,
|
||||
@@ -143,6 +146,42 @@ class MatchingScripts(object):
|
||||
idle = attr.ib(default=attr.Factory(list))
|
||||
|
||||
|
||||
class GreasemonkeyMatcher:
|
||||
|
||||
"""Check whether scripts should be loaded for a given URL."""
|
||||
|
||||
# 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, url):
|
||||
self._url = url
|
||||
self._url_string = url.toString(QUrl.FullyEncoded)
|
||||
self.is_greaseable = url.scheme() in self.GREASEABLE_SCHEMES
|
||||
|
||||
def _match_pattern(self, pattern):
|
||||
# For include and exclude rules if they start and end with '/' they
|
||||
# should be treated as a (ecma syntax) regular expression.
|
||||
if pattern.startswith('/') and pattern.endswith('/'):
|
||||
matches = re.search(pattern[1:-1], self._url_string, flags=re.I)
|
||||
return matches is not None
|
||||
|
||||
# Otherwise they are glob expressions.
|
||||
return fnmatch.fnmatch(self._url_string, pattern)
|
||||
|
||||
def matches(self, script):
|
||||
"""Check whether the URL matches filtering rules of the script."""
|
||||
assert self.is_greaseable
|
||||
matching_includes = any(self._match_pattern(pat)
|
||||
for pat in script.includes)
|
||||
matching_match = any(urlmatch.UrlPattern(pat).matches(self._url)
|
||||
for pat in script.matches)
|
||||
matching_excludes = any(self._match_pattern(pat)
|
||||
for pat in script.excludes)
|
||||
return (matching_includes or matching_match) and not matching_excludes
|
||||
|
||||
|
||||
class GreasemonkeyManager(QObject):
|
||||
|
||||
"""Manager of userscripts and a Greasemonkey compatible environment.
|
||||
@@ -154,10 +193,6 @@ class GreasemonkeyManager(QObject):
|
||||
"""
|
||||
|
||||
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)
|
||||
@@ -309,30 +344,17 @@ class GreasemonkeyManager(QObject):
|
||||
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:
|
||||
matcher = GreasemonkeyMatcher(url)
|
||||
if not matcher.is_greaseable:
|
||||
return MatchingScripts(url, [], [], [])
|
||||
|
||||
string_url = url.toString(QUrl.FullyEncoded)
|
||||
|
||||
def _match(pattern):
|
||||
# For include and exclude rules if they start and end with '/' they
|
||||
# should be treated as a (ecma syntax) regular expression.
|
||||
if pattern.startswith('/') and pattern.endswith('/'):
|
||||
matches = re.search(pattern[1:-1], string_url, flags=re.I)
|
||||
return matches is not None
|
||||
|
||||
# Otherwise they are glob expressions.
|
||||
return fnmatch.fnmatch(string_url, pattern)
|
||||
|
||||
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)]
|
||||
url=url,
|
||||
start=[script for script in self._run_start
|
||||
if matcher.matches(script)],
|
||||
end=[script for script in self._run_end
|
||||
if matcher.matches(script)],
|
||||
idle=[script for script in self._run_idle
|
||||
if matcher.matches(script)]
|
||||
)
|
||||
|
||||
def all_scripts(self):
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
from PyQt5.QtCore import QObject, QEvent, Qt, QTimer
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import message, log, usertypes
|
||||
from qutebrowser.utils import message, log, usertypes, qtutils
|
||||
from qutebrowser.keyinput import modeman
|
||||
|
||||
|
||||
@@ -54,6 +54,14 @@ class ChildEventFilter(QObject):
|
||||
obj, child))
|
||||
assert obj is self._widget
|
||||
child.installEventFilter(self._filter)
|
||||
|
||||
if qtutils.version_check('5.11', compiled=False, exact=True):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
|
||||
QTimer.singleShot(0, self._widget.setFocus)
|
||||
elif event.type() == QEvent.ChildRemoved:
|
||||
child = event.child()
|
||||
log.mouse.debug("{}: removed child {}".format(obj, child))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ Module attributes:
|
||||
_HANDLERS: The handlers registered via decorators.
|
||||
"""
|
||||
|
||||
import html
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
@@ -123,12 +124,12 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
|
||||
def wrong_backend_handler(self, url):
|
||||
"""Show an error page about using the invalid backend."""
|
||||
html = jinja.render('error.html',
|
||||
title="Error while opening qute://url",
|
||||
url=url.toDisplayString(),
|
||||
error='{} is not available with this '
|
||||
'backend'.format(url.toDisplayString()))
|
||||
return 'text/html', html
|
||||
src = jinja.render('error.html',
|
||||
title="Error while opening qute://url",
|
||||
url=url.toDisplayString(),
|
||||
error='{} is not available with this '
|
||||
'backend'.format(url.toDisplayString()))
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
def data_for_url(url):
|
||||
@@ -196,11 +197,11 @@ def qute_bookmarks(_url):
|
||||
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
|
||||
key=lambda x: x[0]) # Sort by name
|
||||
|
||||
html = jinja.render('bookmarks.html',
|
||||
title='Bookmarks',
|
||||
bookmarks=bookmarks,
|
||||
quickmarks=quickmarks)
|
||||
return 'text/html', html
|
||||
src = jinja.render('bookmarks.html',
|
||||
title='Bookmarks',
|
||||
bookmarks=bookmarks,
|
||||
quickmarks=quickmarks)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('tabs')
|
||||
@@ -218,10 +219,10 @@ def qute_tabs(_url):
|
||||
urlstr = tab.url().toDisplayString()
|
||||
tabs[str(win_id)].append((tab.title(), urlstr))
|
||||
|
||||
html = jinja.render('tabs.html',
|
||||
title='Tabs',
|
||||
tab_list_by_window=tabs)
|
||||
return 'text/html', html
|
||||
src = jinja.render('tabs.html',
|
||||
title='Tabs',
|
||||
tab_list_by_window=tabs)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
def history_data(start_time, offset=None):
|
||||
@@ -241,8 +242,9 @@ def history_data(start_time, offset=None):
|
||||
end_time = start_time - 24*60*60
|
||||
entries = hist.entries_between(end_time, start_time)
|
||||
|
||||
return [{"url": e.url, "title": e.title or e.url, "time": e.atime}
|
||||
for e in entries]
|
||||
return [{"url": html.escape(e.url),
|
||||
"title": html.escape(e.title) or html.escape(e.url),
|
||||
"time": e.atime} for e in entries]
|
||||
|
||||
|
||||
@add_handler('history')
|
||||
@@ -287,25 +289,25 @@ def qute_javascript(url):
|
||||
@add_handler('pyeval')
|
||||
def qute_pyeval(_url):
|
||||
"""Handler for qute://pyeval."""
|
||||
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
||||
return 'text/html', html
|
||||
src = jinja.render('pre.html', title='pyeval', content=pyeval_output)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('spawn-output')
|
||||
def qute_spawn_output(_url):
|
||||
"""Handler for qute://spawn-output."""
|
||||
html = jinja.render('pre.html', title='spawn output', content=spawn_output)
|
||||
return 'text/html', html
|
||||
src = jinja.render('pre.html', title='spawn output', content=spawn_output)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('version')
|
||||
@add_handler('verizon')
|
||||
def qute_version(_url):
|
||||
"""Handler for qute://version."""
|
||||
html = jinja.render('version.html', title='Version info',
|
||||
version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
return 'text/html', html
|
||||
src = jinja.render('version.html', title='Version info',
|
||||
version=version.version(),
|
||||
copyright=qutebrowser.__copyright__)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('plainlog')
|
||||
@@ -323,8 +325,8 @@ def qute_plainlog(url):
|
||||
if not level:
|
||||
level = 'vdebug'
|
||||
text = log.ram_handler.dump_log(html=False, level=level)
|
||||
html = jinja.render('pre.html', title='log', content=text)
|
||||
return 'text/html', html
|
||||
src = jinja.render('pre.html', title='log', content=text)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('log')
|
||||
@@ -343,8 +345,8 @@ def qute_log(url):
|
||||
level = 'vdebug'
|
||||
html_log = log.ram_handler.dump_log(html=True, level=level)
|
||||
|
||||
html = jinja.render('log.html', title='log', content=html_log)
|
||||
return 'text/html', html
|
||||
src = jinja.render('log.html', title='log', content=html_log)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('gpl')
|
||||
@@ -415,12 +417,12 @@ def qute_help(url):
|
||||
@add_handler('backend-warning')
|
||||
def qute_backend_warning(_url):
|
||||
"""Handler for qute://backend-warning."""
|
||||
html = jinja.render('backend-warning.html',
|
||||
distribution=version.distribution(),
|
||||
Distribution=version.Distribution,
|
||||
version=pkg_resources.parse_version,
|
||||
title="Legacy backend warning")
|
||||
return 'text/html', html
|
||||
src = jinja.render('backend-warning.html',
|
||||
distribution=version.distribution(),
|
||||
Distribution=version.Distribution,
|
||||
version=pkg_resources.parse_version,
|
||||
title="Legacy backend warning")
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
def _qute_settings_set(url):
|
||||
@@ -450,10 +452,10 @@ def qute_settings(url):
|
||||
if url.path() == '/set':
|
||||
return _qute_settings_set(url)
|
||||
|
||||
html = jinja.render('settings.html', title='settings',
|
||||
configdata=configdata,
|
||||
confget=config.instance.get_str)
|
||||
return 'text/html', html
|
||||
src = jinja.render('settings.html', title='settings',
|
||||
configdata=configdata,
|
||||
confget=config.instance.get_str)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('bindings')
|
||||
@@ -467,9 +469,9 @@ def qute_bindings(_url):
|
||||
for mode in modes:
|
||||
bindings[mode] = config.key_instance.get_bindings_for(mode)
|
||||
|
||||
html = jinja.render('bindings.html', title='Bindings',
|
||||
bindings=bindings)
|
||||
return 'text/html', html
|
||||
src = jinja.render('bindings.html', title='Bindings',
|
||||
bindings=bindings)
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('back')
|
||||
@@ -478,10 +480,10 @@ def qute_back(url):
|
||||
|
||||
Simple page to free ram / lazy load a site, goes back on focusing the tab.
|
||||
"""
|
||||
html = jinja.render(
|
||||
src = jinja.render(
|
||||
'back.html',
|
||||
title='Suspended: ' + urllib.parse.unquote(url.fragment()))
|
||||
return 'text/html', html
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
@add_handler('configdiff')
|
||||
|
||||
@@ -307,6 +307,10 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
href_tags = ['a', 'area', 'link']
|
||||
return self.tag_name() in href_tags and 'href' in self
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
"""Return True if clicking this element needs user interaction."""
|
||||
raise NotImplementedError
|
||||
|
||||
def _mouse_pos(self):
|
||||
"""Get the position to click/hover."""
|
||||
# Click the center of the largest square fitting into the top/left
|
||||
@@ -405,7 +409,7 @@ class AbstractWebElement(collections.abc.MutableMapping):
|
||||
return
|
||||
|
||||
if click_target == usertypes.ClickTarget.normal:
|
||||
if self.is_link():
|
||||
if self.is_link() and not self._requires_user_interaction():
|
||||
log.webelem.debug("Clicking via JS click()")
|
||||
self._click_js(click_target)
|
||||
elif self.is_editable(strict=True):
|
||||
|
||||
@@ -24,16 +24,18 @@ import os
|
||||
import re
|
||||
|
||||
from PyQt5.QtCore import QLibraryInfo
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.utils import log, message
|
||||
|
||||
dict_version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
|
||||
|
||||
|
||||
def version(filename):
|
||||
"""Extract the version number from the dictionary file name."""
|
||||
version_re = re.compile(r".+-(?P<version>[0-9]+-[0-9]+?)\.bdic")
|
||||
match = version_re.fullmatch(filename)
|
||||
match = dict_version_re.match(filename)
|
||||
if match is None:
|
||||
raise ValueError('the given dictionary file name is malformed: {}'
|
||||
.format(filename))
|
||||
message.warning(
|
||||
"Found a dictionary with a malformed name: {}".format(filename))
|
||||
return None
|
||||
return tuple(int(n) for n in match.group('version').split('-'))
|
||||
|
||||
|
||||
@@ -44,15 +46,23 @@ def dictionary_dir():
|
||||
|
||||
|
||||
def local_files(code):
|
||||
"""Return all installed dictionaries for the given code."""
|
||||
"""Return all installed dictionaries for the given code.
|
||||
|
||||
The returned dictionaries are sorted by version, therefore the latest will
|
||||
be the first element. The list will be empty if no dictionaries are found.
|
||||
"""
|
||||
pathname = os.path.join(dictionary_dir(), '{}*.bdic'.format(code))
|
||||
matching_dicts = glob.glob(pathname)
|
||||
files = []
|
||||
for matching_dict in sorted(matching_dicts, key=version, reverse=True):
|
||||
filename = os.path.basename(matching_dict)
|
||||
log.config.debug('Found file for dict {}: {}'.format(code, filename))
|
||||
files.append(filename)
|
||||
return files
|
||||
versioned_dicts = []
|
||||
for matching_dict in matching_dicts:
|
||||
parsed_version = version(matching_dict)
|
||||
if parsed_version is not None:
|
||||
filename = os.path.basename(matching_dict)
|
||||
log.config.debug('Found file for dict {}: {}'
|
||||
.format(code, filename))
|
||||
versioned_dicts.append((parsed_version, filename))
|
||||
return [filename for version, filename
|
||||
in sorted(versioned_dicts, reverse=True)]
|
||||
|
||||
|
||||
def local_filename(code):
|
||||
|
||||
@@ -101,7 +101,11 @@ class DownloadItem(downloads.AbstractDownloadItem):
|
||||
|
||||
def retry(self):
|
||||
state = self._qt_item.state()
|
||||
assert state == QWebEngineDownloadItem.DownloadInterrupted, state
|
||||
if state != QWebEngineDownloadItem.DownloadInterrupted:
|
||||
log.downloads.warning(
|
||||
"Trying to retry download in state {}".format(
|
||||
debug.qenum_key(QWebEngineDownloadItem, state)))
|
||||
return
|
||||
|
||||
try:
|
||||
self._qt_item.resume()
|
||||
|
||||
@@ -27,7 +27,7 @@ from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineSettings
|
||||
|
||||
from qutebrowser.utils import log, javascript
|
||||
from qutebrowser.utils import log, javascript, urlutils
|
||||
from qutebrowser.browser import webelem
|
||||
|
||||
|
||||
@@ -198,6 +198,13 @@ class WebEngineElement(webelem.AbstractWebElement):
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._js_call('move_cursor_to_end')
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
baseurl = self._tab.url()
|
||||
url = self.resolve_url(baseurl)
|
||||
if url is None:
|
||||
return True
|
||||
return url.scheme() not in urlutils.WEBENGINE_SCHEMES
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-58515
|
||||
ev = QMouseEvent(QMouseEvent.MouseButtonPress, QPoint(0, 0),
|
||||
|
||||
@@ -34,6 +34,9 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
def install(self, profile):
|
||||
"""Install the handler for qute:// URLs on the given profile."""
|
||||
profile.installUrlSchemeHandler(b'qute', self)
|
||||
if qtutils.version_check('5.11', compiled=False):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
|
||||
profile.installUrlSchemeHandler(b'chrome-error', self)
|
||||
|
||||
def requestStarted(self, job):
|
||||
"""Handle a request for a qute: scheme.
|
||||
@@ -45,6 +48,12 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
job: QWebEngineUrlRequestJob
|
||||
"""
|
||||
url = job.requestUrl()
|
||||
|
||||
if url.scheme() == 'chrome-error':
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-63378
|
||||
job.fail(QWebEngineUrlRequestJob.UrlInvalid)
|
||||
return
|
||||
|
||||
assert job.requestMethod() == b'GET'
|
||||
assert url.scheme() == 'qute'
|
||||
log.misc.debug("Got request for {}".format(url.toDisplayString()))
|
||||
|
||||
@@ -176,24 +176,11 @@ class ProfileSetter:
|
||||
"""Initialize settings on the given profile."""
|
||||
self.set_http_headers()
|
||||
self.set_http_cache_size()
|
||||
self._init_attributes()
|
||||
|
||||
self._profile.settings().setAttribute(
|
||||
QWebEngineSettings.FullScreenSupportEnabled, True)
|
||||
if qtutils.version_check('5.8'):
|
||||
self._profile.setSpellCheckEnabled(True)
|
||||
self.set_dictionary_language()
|
||||
|
||||
def _init_attributes(self):
|
||||
"""Initialize hard-coded attributes."""
|
||||
values = {
|
||||
'FullScreenSupportEnabled': True,
|
||||
'FocusOnNavigationEnabled': True,
|
||||
}
|
||||
settings = self._profile.settings()
|
||||
for name, value in values.items():
|
||||
attr = getattr(QWebEngineSettings, name, None)
|
||||
if attr is not None:
|
||||
settings.setAttribute(attr, value)
|
||||
|
||||
def set_http_headers(self):
|
||||
"""Set the user agent and accept-language for the given profile.
|
||||
|
||||
@@ -242,6 +229,7 @@ class ProfileSetter:
|
||||
|
||||
log.config.debug("Found dicts: {}".format(filenames))
|
||||
self._profile.setSpellCheckLanguages(filenames)
|
||||
self._profile.setSpellCheckEnabled(bool(filenames))
|
||||
|
||||
|
||||
def _update_settings(option):
|
||||
|
||||
@@ -950,11 +950,10 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
@pyqtSlot()
|
||||
def _on_load_started(self):
|
||||
"""Clear search when a new load is started if needed."""
|
||||
if (qtutils.version_check('5.9', compiled=False) and
|
||||
not qtutils.version_check('5.9.2', compiled=False)):
|
||||
# WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-61506
|
||||
self.search.clear()
|
||||
# WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-61506
|
||||
# (seems to be back in later Qt versions as well)
|
||||
self.search.clear()
|
||||
super()._on_load_started()
|
||||
self.data.netrc_used = False
|
||||
|
||||
@@ -1026,8 +1025,9 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
log.config.debug(
|
||||
"Loading {} again because of config change".format(
|
||||
self._reload_url.toDisplayString()))
|
||||
QTimer.singleShot(100, lambda url=self._reload_url:
|
||||
self.openurl(url, predict=False))
|
||||
QTimer.singleShot(100, functools.partial(self.openurl,
|
||||
self._reload_url,
|
||||
predict=False))
|
||||
self._reload_url = None
|
||||
|
||||
if not qtutils.version_check('5.10', compiled=False):
|
||||
@@ -1046,10 +1046,11 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
@pyqtSlot(usertypes.NavigationRequest)
|
||||
def _on_navigation_request(self, navigation):
|
||||
super()._on_navigation_request(navigation)
|
||||
|
||||
if not navigation.accepted or not navigation.is_main_frame:
|
||||
return
|
||||
|
||||
needs_reload = {
|
||||
settings_needing_reload = {
|
||||
'content.plugins',
|
||||
'content.javascript.enabled',
|
||||
'content.javascript.can_access_clipboard',
|
||||
@@ -1058,11 +1059,20 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
'input.spatial_navigation',
|
||||
'input.spatial_navigation',
|
||||
}
|
||||
assert needs_reload.issubset(configdata.DATA)
|
||||
assert settings_needing_reload.issubset(configdata.DATA)
|
||||
|
||||
changed = self.settings.update_for_url(navigation.url)
|
||||
if (changed & needs_reload and navigation.navigation_type !=
|
||||
navigation.Type.link_clicked):
|
||||
reload_needed = changed & settings_needing_reload
|
||||
|
||||
# On Qt < 5.11, we don't don't need a reload when type == link_clicked.
|
||||
# On Qt 5.11.0, we always need a reload.
|
||||
# TODO on Qt > 5.11.0, we hopefully never need a reload:
|
||||
# https://codereview.qt-project.org/#/c/229525/1
|
||||
if not qtutils.version_check('5.11.0', exact=True, compiled=False):
|
||||
if navigation.navigation_type == navigation.Type.link_clicked:
|
||||
reload_needed = False
|
||||
|
||||
if reload_needed:
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-66656
|
||||
self._reload_url = navigation.url
|
||||
|
||||
@@ -1104,6 +1114,4 @@ class WebEngineTab(browsertab.AbstractTab):
|
||||
self.predicted_navigation.connect(self._on_predicted_navigation)
|
||||
|
||||
def event_target(self):
|
||||
fp = self._widget.focusProxy()
|
||||
assert fp is not None
|
||||
return fp
|
||||
return self._widget.render_widget()
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
|
||||
import functools
|
||||
|
||||
import sip
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QUrl, PYQT_VERSION
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PyQt5.QtWebEngineWidgets import (QWebEngineView, QWebEnginePage,
|
||||
QWebEngineScript)
|
||||
|
||||
@@ -30,6 +32,7 @@ from qutebrowser.browser import shared
|
||||
from qutebrowser.browser.webengine import certificateerror, webenginesettings
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import log, debug, usertypes, jinja, objreg, qtutils
|
||||
from qutebrowser.misc import miscwidgets
|
||||
|
||||
|
||||
class WebEngineView(QWebEngineView):
|
||||
@@ -51,6 +54,34 @@ class WebEngineView(QWebEngineView):
|
||||
parent=self)
|
||||
self.setPage(page)
|
||||
|
||||
if qtutils.version_check('5.11', compiled=False):
|
||||
# Set a PseudoLayout as a WORKAROUND for
|
||||
# https://bugreports.qt.io/browse/QTBUG-68224
|
||||
# and other related issues.
|
||||
sip.delete(self.layout())
|
||||
self._layout = miscwidgets.PseudoLayout(self)
|
||||
|
||||
def render_widget(self):
|
||||
"""Get the RenderWidgetHostViewQt for this view.
|
||||
|
||||
Normally, this would always be the focusProxy().
|
||||
However, it sometimes isn't, so we use this as a WORKAROUND for
|
||||
https://bugreports.qt.io/browse/QTBUG-68727
|
||||
"""
|
||||
proxy = self.focusProxy()
|
||||
if proxy is not None:
|
||||
return proxy
|
||||
|
||||
# We don't want e.g. a QMenu.
|
||||
rwhv_class = 'QtWebEngineCore::RenderWidgetHostViewQtDelegateWidget'
|
||||
children = [c for c in self.findChildren(QWidget)
|
||||
if c.isVisible() and c.inherits(rwhv_class)]
|
||||
|
||||
log.webview.debug("Found possibly lost focusProxy: {}"
|
||||
.format(children))
|
||||
|
||||
return children[-1] if children else None
|
||||
|
||||
def shutdown(self):
|
||||
self.page().shutdown()
|
||||
|
||||
|
||||
@@ -305,6 +305,9 @@ class WebKitElement(webelem.AbstractWebElement):
|
||||
if self.is_text_input() and self.is_editable():
|
||||
self._tab.caret.move_to_end_of_document()
|
||||
|
||||
def _requires_user_interaction(self):
|
||||
return False
|
||||
|
||||
def _click_editable(self, click_target):
|
||||
ok = self._elem.evaluateJavaScript('this.focus(); true;')
|
||||
if ok:
|
||||
|
||||
@@ -74,7 +74,7 @@ function tryagain()
|
||||
</td>
|
||||
<td style="padding-left: 40px;">
|
||||
<h1>Unable to load page</h1>
|
||||
Error while opening {{ url }}<br>
|
||||
Error while opening {{ url | default('page', true) }}<br>
|
||||
<p id="error-message-text" style="color: #a31a1a;">{{ error }}</p><br><br>
|
||||
|
||||
<form name="bl">
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
"""Base class for vim-like key sequence parser."""
|
||||
|
||||
import string
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject
|
||||
from PyQt5.QtGui import QKeySequence
|
||||
|
||||
@@ -136,7 +138,7 @@ class BaseKeyParser(QObject):
|
||||
def _match_count(self, sequence, dry_run):
|
||||
"""Try to match a key as count."""
|
||||
txt = str(sequence[-1]) # To account for sequences changed above.
|
||||
if (txt.isdigit() and self._supports_count and
|
||||
if (txt in string.digits and self._supports_count and
|
||||
not (not self._count and txt == '0')):
|
||||
self._debug_log("Trying match as count")
|
||||
assert len(txt) == 1, txt
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
"""The commandline in the statusbar."""
|
||||
|
||||
import functools
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QSize
|
||||
from PyQt5.QtWidgets import QSizePolicy
|
||||
|
||||
@@ -69,6 +71,26 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
self.textChanged.connect(self.updateGeometry)
|
||||
self.textChanged.connect(self._incremental_search)
|
||||
|
||||
self._command_dispatcher = objreg.get(
|
||||
'command-dispatcher', scope='window', window=self._win_id)
|
||||
|
||||
def _handle_search(self):
|
||||
"""Check if the currently entered text is a search, and if so, run it.
|
||||
|
||||
Return:
|
||||
True if a search was executed, False otherwise.
|
||||
"""
|
||||
search_prefixes = {
|
||||
'/': self._command_dispatcher.search,
|
||||
'?': functools.partial(
|
||||
self._command_dispatcher.search, reverse=True)
|
||||
}
|
||||
if self.prefix() in search_prefixes:
|
||||
search_fn = search_prefixes[self.prefix()]
|
||||
search_fn(self.text()[1:])
|
||||
return True
|
||||
return False
|
||||
|
||||
def prefix(self):
|
||||
"""Get the currently entered command prefix."""
|
||||
text = self.text()
|
||||
@@ -162,17 +184,17 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
Args:
|
||||
rapid: Run the command without closing or clearing the command bar.
|
||||
"""
|
||||
prefixes = {
|
||||
':': '',
|
||||
'/': 'search -- ',
|
||||
'?': 'search -r -- ',
|
||||
}
|
||||
text = self.text()
|
||||
self.history.append(text)
|
||||
|
||||
was_search = self._handle_search()
|
||||
|
||||
if not rapid:
|
||||
modeman.leave(self._win_id, usertypes.KeyMode.command,
|
||||
'cmd accept')
|
||||
self.got_cmd[str].emit(prefixes[text[0]] + text[1:])
|
||||
|
||||
if not was_search:
|
||||
self.got_cmd[str].emit(text[1:])
|
||||
|
||||
@cmdutils.register(instance='status-command', scope='window')
|
||||
def edit_command(self, run=False):
|
||||
@@ -253,15 +275,9 @@ class Command(misc.MinimalLineEditMixin, misc.CommandLineEdit):
|
||||
width = self.fontMetrics().width(text)
|
||||
return QSize(width, height)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _incremental_search(self, text):
|
||||
@pyqtSlot()
|
||||
def _incremental_search(self):
|
||||
if not config.val.search.incremental:
|
||||
return
|
||||
|
||||
search_prefixes = {
|
||||
'/': 'search -- ',
|
||||
'?': 'search -r -- ',
|
||||
}
|
||||
|
||||
if self.prefix() in ['/', '?']:
|
||||
self.got_cmd[str].emit(search_prefixes[text[0]] + text[1:])
|
||||
self._handle_search()
|
||||
|
||||
@@ -489,6 +489,9 @@ class TabbedBrowser(QWidget):
|
||||
self.widget.count())
|
||||
else:
|
||||
self.widget.setCurrentWidget(tab)
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-68076
|
||||
# Still seems to be needed with Qt 5.11.1
|
||||
tab.setFocus()
|
||||
|
||||
tab.show()
|
||||
self.new_tab.emit(tab, idx)
|
||||
|
||||
@@ -45,7 +45,7 @@ class PyPIVersionClient(QObject):
|
||||
arg: The error message, as string.
|
||||
"""
|
||||
|
||||
API_URL = 'https://pypi.python.org/pypi/{}/json'
|
||||
API_URL = 'https://pypi.org/pypi/{}/json'
|
||||
success = pyqtSignal(str)
|
||||
error = pyqtSignal(str)
|
||||
|
||||
|
||||
@@ -28,6 +28,21 @@ from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkRequest,
|
||||
QNetworkReply)
|
||||
|
||||
|
||||
class HTTPRequest(QNetworkRequest):
|
||||
"""A QNetworkRquest that follows (secure) redirects by default."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
try:
|
||||
self.setAttribute(QNetworkRequest.RedirectPolicyAttribute,
|
||||
QNetworkRequest.NoLessSafeRedirectPolicy)
|
||||
except AttributeError:
|
||||
# RedirectPolicyAttribute was introduced in 5.9 to replace
|
||||
# FollowRedirectsAttribute.
|
||||
self.setAttribute(QNetworkRequest.FollowRedirectsAttribute,
|
||||
True)
|
||||
|
||||
|
||||
class HTTPClient(QObject):
|
||||
|
||||
"""An HTTP client based on QNetworkAccessManager.
|
||||
@@ -63,7 +78,7 @@ class HTTPClient(QObject):
|
||||
if data is None:
|
||||
data = {}
|
||||
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
|
||||
request = QNetworkRequest(url)
|
||||
request = HTTPRequest(url)
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader,
|
||||
'application/x-www-form-urlencoded;charset=utf-8')
|
||||
reply = self._nam.post(request, encoded_data)
|
||||
@@ -77,7 +92,7 @@ class HTTPClient(QObject):
|
||||
Args:
|
||||
url: The URL to access, as QUrl.
|
||||
"""
|
||||
request = QNetworkRequest(url)
|
||||
request = HTTPRequest(url)
|
||||
reply = self._nam.get(request)
|
||||
self._handle_reply(reply)
|
||||
|
||||
|
||||
@@ -266,6 +266,47 @@ class WrapperLayout(QLayout):
|
||||
self._widget.deleteLater()
|
||||
|
||||
|
||||
class PseudoLayout(QLayout):
|
||||
|
||||
"""A layout which isn't actually a real layout.
|
||||
|
||||
This is used to replace QWebEngineView's internal layout, as a WORKAROUND
|
||||
for https://bugreports.qt.io/browse/QTBUG-68224 and other related issues.
|
||||
|
||||
This is partly inspired by https://codereview.qt-project.org/#/c/230894/
|
||||
which does something similar as part of Qt.
|
||||
"""
|
||||
|
||||
def addItem(self, item):
|
||||
assert self.parent() is not None
|
||||
item.widget().setParent(self.parent())
|
||||
|
||||
def removeItem(self, item):
|
||||
item.widget().setParent(None)
|
||||
|
||||
def count(self):
|
||||
return 0
|
||||
|
||||
def itemAt(self, _pos):
|
||||
return None
|
||||
|
||||
def widget(self):
|
||||
return self.parent().render_widget()
|
||||
|
||||
def setGeometry(self, rect):
|
||||
"""Resize the render widget when the view is resized."""
|
||||
widget = self.widget()
|
||||
if widget is not None:
|
||||
widget.setGeometry(rect)
|
||||
|
||||
def sizeHint(self):
|
||||
"""Make sure the view has the sizeHint of the render widget."""
|
||||
widget = self.widget()
|
||||
if widget is not None:
|
||||
return widget.sizeHint()
|
||||
return QSize()
|
||||
|
||||
|
||||
class FullscreenNotification(QLabel):
|
||||
|
||||
"""A label telling the user this page is now fullscreen."""
|
||||
|
||||
@@ -39,6 +39,21 @@ from qutebrowser.browser.network import pac
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/108
|
||||
|
||||
|
||||
# URL schemes supported by QtWebEngine
|
||||
WEBENGINE_SCHEMES = [
|
||||
'about',
|
||||
'data',
|
||||
'file',
|
||||
'filesystem',
|
||||
'ftp',
|
||||
'http',
|
||||
'https',
|
||||
'javascript',
|
||||
'ws',
|
||||
'wss',
|
||||
]
|
||||
|
||||
|
||||
class InvalidUrlError(ValueError):
|
||||
|
||||
"""Error raised if a function got an invalid URL.
|
||||
|
||||
@@ -34,6 +34,11 @@ import tarfile
|
||||
import tempfile
|
||||
import collections
|
||||
|
||||
try:
|
||||
import winreg
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
|
||||
os.pardir))
|
||||
|
||||
@@ -222,8 +227,25 @@ def build_windows():
|
||||
utils.print_title("Building Windows binaries")
|
||||
parts = str(sys.version_info.major), str(sys.version_info.minor)
|
||||
ver = ''.join(parts)
|
||||
python_x86 = r'C:\Python{}-32\python.exe'.format(ver)
|
||||
python_x64 = r'C:\Python{}\python.exe'.format(ver)
|
||||
dot_ver = '.'.join(parts)
|
||||
|
||||
# Get python path from registry if possible
|
||||
try:
|
||||
reg64_key = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE,
|
||||
r'SOFTWARE\Python\PythonCore'
|
||||
r'\{}\InstallPath'.format(dot_ver))
|
||||
python_x64 = winreg.QueryValueEx(reg64_key, 'ExecutablePath')[0]
|
||||
except FileNotFoundError:
|
||||
python_x64 = r'C:\Python{}\python.exe'.format(ver)
|
||||
|
||||
try:
|
||||
reg32_key = winreg.OpenKeyEx(winreg.HKEY_LOCAL_MACHINE,
|
||||
r'SOFTWARE\WOW6432Node\Python\PythonCore'
|
||||
r'\{}-32\InstallPath'.format(dot_ver))
|
||||
python_x86 = winreg.QueryValueEx(reg32_key, 'ExecutablePath')[0]
|
||||
except FileNotFoundError:
|
||||
python_x86 = r'C:\Python{}-32\python.exe'.format(ver)
|
||||
|
||||
out_pyinstaller = os.path.join('dist', 'qutebrowser')
|
||||
out_32 = os.path.join('dist',
|
||||
'qutebrowser-{}-x86'.format(qutebrowser.__version__))
|
||||
|
||||
1
setup.py
1
setup.py
@@ -77,6 +77,7 @@ try:
|
||||
version='.'.join(str(e) for e in _get_constant('version_info')),
|
||||
description=_get_constant('description'),
|
||||
long_description=read_file('README.asciidoc'),
|
||||
long_description_content_type='text/plain',
|
||||
url='https://www.qutebrowser.org/',
|
||||
author=_get_constant('author'),
|
||||
author_email=_get_constant('email'),
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<a href="what://::">I'm broken</a>
|
||||
<a href="foo://bar">Unknown scheme</a>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
10
tests/end2end/data/issue4011.html
Normal file
10
tests/end2end/data/issue4011.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><img src="x" onerror="console.log('XSS')">foo</title>
|
||||
</head>
|
||||
<body>
|
||||
foo
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,6 +16,8 @@
|
||||
BAZ<br/>
|
||||
space travel<br/>
|
||||
/slash<br/>
|
||||
-r reversed<br/>
|
||||
;; semicolons<br/>
|
||||
<a class="toselect" href="hello.txt">follow me!</a><br/>
|
||||
</p>
|
||||
</body>
|
||||
|
||||
@@ -201,8 +201,8 @@ Feature: Downloading things from a website.
|
||||
And I run :download-retry
|
||||
Then the error "Retrying downloads is unsupported *" should be shown
|
||||
|
||||
@qtwebkit_skip @qt>=5.10
|
||||
Scenario: Retrying a failed download with QtWebEngine (Qt >= 5.10)
|
||||
@qtwebkit_skip @qt==5.10.1
|
||||
Scenario: Retrying a failed download with QtWebEngine (Qt 5.10)
|
||||
When I open data/downloads/issue2298.html
|
||||
And I run :click-element id download
|
||||
And I wait for "Download error: *" in the log
|
||||
|
||||
@@ -156,11 +156,13 @@ Feature: Using hints
|
||||
And I hint with args "all run message-info {hint-url}" and follow a
|
||||
Then the message "http://localhost:(port)/data/hello.txt" should be shown
|
||||
|
||||
@qt!=5.11.0
|
||||
Scenario: Clicking an invalid link
|
||||
When I open data/invalid_link.html
|
||||
And I hint with args "all" and follow a
|
||||
Then the error "Invalid link clicked - *" should be shown
|
||||
|
||||
@qt!=5.11.0
|
||||
Scenario: Clicking an invalid link opening in a new tab
|
||||
When I open data/invalid_link.html
|
||||
And I hint with args "all tab" and follow a
|
||||
|
||||
@@ -111,3 +111,8 @@ Feature: Page history
|
||||
And I wait until qute://history is loaded
|
||||
Then the page should contain the plaintext "3.txt"
|
||||
Then the page should contain the plaintext "4.txt"
|
||||
|
||||
Scenario: XSS in :history
|
||||
When I open data/issue4011.html
|
||||
And I open qute://history
|
||||
Then the javascript message "XSS" should not be logged
|
||||
|
||||
@@ -40,11 +40,26 @@ Feature: Searching on a page
|
||||
Then "space " should be found
|
||||
|
||||
Scenario: Searching with / and slash in search term (issue 507)
|
||||
When I run :set-cmd-text -s //slash
|
||||
When I run :set-cmd-text //slash
|
||||
And I run :command-accept
|
||||
And I wait for "search found /slash" in the log
|
||||
Then "/slash" should be found
|
||||
|
||||
Scenario: Searching with arguments at start of search term
|
||||
When I run :set-cmd-text /-r reversed
|
||||
And I run :command-accept
|
||||
And I wait for "search found -r reversed" in the log
|
||||
Then "-r reversed" should be found
|
||||
|
||||
Scenario: Searching with semicolons in search term
|
||||
When I run :set-cmd-text /;
|
||||
And I run :fake-key -g ;
|
||||
And I run :fake-key -g <space>
|
||||
And I run :fake-key -g semi
|
||||
And I run :command-accept
|
||||
And I wait for "search found ;; semi" in the log
|
||||
Then ";; semi" should be found
|
||||
|
||||
# This doesn't work because this is QtWebKit behavior.
|
||||
@xfail_norun
|
||||
Scenario: Searching text with umlauts
|
||||
|
||||
@@ -361,6 +361,9 @@ class QuteProc(testprocess.Process):
|
||||
"Focus object changed: "
|
||||
"<qutebrowser.browser.webengine.webview.WebEngineView object "
|
||||
"at *>",
|
||||
# Qt >= 5.11 with workarounds
|
||||
"Focus object changed: "
|
||||
"<PyQt5.QtQuickWidgets.QQuickWidget object at *>",
|
||||
]
|
||||
|
||||
if (log_line.category == 'ipc' and
|
||||
|
||||
@@ -17,31 +17,69 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import pytest
|
||||
"""Tests for qutebrowser.browser.webengine.spell module."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import QLibraryInfo
|
||||
from qutebrowser.browser.webengine import spell
|
||||
from qutebrowser.utils import usertypes
|
||||
|
||||
|
||||
def test_version():
|
||||
def test_version(message_mock, caplog):
|
||||
"""Tests parsing dictionary version from its file name."""
|
||||
assert spell.version('en-US-8-0.bdic') == (8, 0)
|
||||
assert spell.version('pl-PL-3-0.bdic') == (3, 0)
|
||||
with pytest.raises(ValueError):
|
||||
spell.version('malformed_filename')
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert spell.version('malformed_filename') is None
|
||||
msg = message_mock.getmsg(usertypes.MessageLevel.warning)
|
||||
expected = ("Found a dictionary with a malformed name: malformed_filename")
|
||||
assert msg.text == expected
|
||||
|
||||
|
||||
def test_local_filename_dictionary_does_not_exist(tmpdir, monkeypatch):
|
||||
def test_dictionary_dir(monkeypatch):
|
||||
monkeypatch.setattr(QLibraryInfo, 'location', lambda _: 'datapath')
|
||||
assert spell.dictionary_dir() == os.path.join('datapath',
|
||||
'qtwebengine_dictionaries')
|
||||
|
||||
|
||||
def test_local_filename_dictionary_does_not_exist(monkeypatch):
|
||||
"""Tests retrieving local filename when the dir doesn't exits."""
|
||||
monkeypatch.setattr(
|
||||
spell, 'dictionary_dir', lambda: '/some-non-existing-dir')
|
||||
assert not spell.local_filename('en-US')
|
||||
|
||||
|
||||
def test_local_filename_dictionary_not_installed(tmpdir, monkeypatch):
|
||||
"""Tests retrieving local filename when the dict not installed."""
|
||||
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
|
||||
assert not spell.local_filename('en-US')
|
||||
|
||||
|
||||
def test_local_filename_not_installed_malformed(tmpdir, monkeypatch, caplog):
|
||||
"""Tests retrieving local filename when the only file is malformed."""
|
||||
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
|
||||
(tmpdir / 'en-US.bdic').ensure()
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert not spell.local_filename('en-US')
|
||||
|
||||
|
||||
def test_local_filename_dictionary_installed(tmpdir, monkeypatch):
|
||||
"""Tests retrieving local filename when the dict installed."""
|
||||
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
|
||||
for lang_file in ['en-US-11-0.bdic', 'en-US-7-1.bdic', 'pl-PL-3-0.bdic']:
|
||||
(tmpdir / lang_file).ensure()
|
||||
assert spell.local_filename('en-US') == 'en-US-11-0'
|
||||
assert spell.local_filename('pl-PL') == 'pl-PL-3-0'
|
||||
|
||||
|
||||
def test_local_filename_installed_malformed(tmpdir, monkeypatch, caplog):
|
||||
"""Tests retrieving local filename when the dict installed.
|
||||
|
||||
In this usecase, another existing file is malformed."""
|
||||
monkeypatch.setattr(spell, 'dictionary_dir', lambda: str(tmpdir))
|
||||
for lang_file in ['en-US-11-0.bdic', 'en-US-7-1.bdic', 'en-US.bdic']:
|
||||
(tmpdir / lang_file).ensure()
|
||||
with caplog.at_level(logging.WARNING):
|
||||
assert spell.local_filename('en-US') == 'en-US-11-0'
|
||||
|
||||
@@ -73,3 +73,14 @@ def test_existing_dict(config_stub, monkeypatch):
|
||||
webenginesettings.private_profile]:
|
||||
assert profile.isSpellCheckEnabled()
|
||||
assert profile.spellCheckLanguages() == ['en-US-8-0']
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not qtutils.version_check('5.8'), reason="Needs Qt 5.8 or newer")
|
||||
def test_spell_check_disabled(config_stub, monkeypatch):
|
||||
monkeypatch.setattr(objects, 'backend', usertypes.Backend.QtWebEngine)
|
||||
config_stub.val.spellcheck.languages = []
|
||||
webenginesettings._update_settings('spellcheck.languages')
|
||||
for profile in [webenginesettings.default_profile,
|
||||
webenginesettings.private_profile]:
|
||||
assert not profile.isSpellCheckEnabled()
|
||||
|
||||
@@ -32,7 +32,7 @@ test_gm_script = r"""
|
||||
// @name qutebrowser test userscript
|
||||
// @namespace invalid.org
|
||||
// @include http://localhost:*/data/title.html
|
||||
// @match http://trolol*
|
||||
// @match http://*.trolol.com/*
|
||||
// @exclude https://badhost.xxx/*
|
||||
// @run-at document-start
|
||||
// ==/UserScript==
|
||||
@@ -60,7 +60,7 @@ def test_all():
|
||||
|
||||
@pytest.mark.parametrize("url, expected_matches", [
|
||||
# included
|
||||
('http://trololololololo.com/', 1),
|
||||
('http://trolol.com/', 1),
|
||||
# neither included nor excluded
|
||||
('http://aaaaaaaaaa.com/', 0),
|
||||
# excluded
|
||||
|
||||
@@ -320,6 +320,10 @@ class TestCount:
|
||||
keyparser.execute.assert_called_once_with('message-info ccc', 23)
|
||||
assert not keyparser._sequence
|
||||
|
||||
def test_superscript(self, handle_text, keyparser):
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/3743
|
||||
handle_text(Qt.Key_twosuperior, Qt.Key_B, Qt.Key_A)
|
||||
|
||||
def test_count_keystring_update(self, qtbot, handle_text, keyparser):
|
||||
"""Make sure the keystring is updated correctly when entering count."""
|
||||
with qtbot.waitSignals([keyparser.keystring_updated,
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_get_version_success(qtbot):
|
||||
with qtbot.waitSignal(client.success):
|
||||
client.get_version('test')
|
||||
|
||||
assert http_stub.url == QUrl('https://pypi.python.org/pypi/test/json')
|
||||
assert http_stub.url == QUrl(client.API_URL.format('test'))
|
||||
|
||||
|
||||
def test_get_version_error(qtbot):
|
||||
|
||||
Reference in New Issue
Block a user