Compare commits

...

117 Commits
osx ... v1.3.x

Author SHA1 Message Date
Florian Bruhin
718f73be2e Use ImportError for winreg import
This also satisfies pylint.

(cherry picked from commit b67733b781)
2018-06-21 23:51:37 +02:00
bitraid
da4865d408 build_release.py: reformat
(cherry picked from commit b61c99687d)
2018-06-21 23:51:35 +02:00
bitraid
fd4ff3c9ce build_release.py: Get python path from registry on Windows
(cherry picked from commit ddfbe255e7)
2018-06-21 23:51:33 +02:00
Florian Bruhin
ad9b50601c Release v1.3.3 2018-06-21 23:30:51 +02:00
Florian Bruhin
c88a94f1cc Update changelog
(cherry picked from commit 66fc3a30dd)
2018-06-21 23:30:38 +02:00
Florian Bruhin
13332cd2cf Fix shadowing of 'html' name
(cherry picked from commit 0864ad4069)
2018-06-21 22:34:05 +02:00
Florian Bruhin
48808e59cb Re-add waiting for QQuickWidget
Apparently this is still needed on some PyQt versions.

(cherry picked from commit 9a5439e5d0)
2018-06-21 22:23:58 +02:00
Florian Bruhin
5571ef4e62 Revert "Properly add QtQuickWidgets dependency"
Looks like FreeBSD doesn't have QtQuickWidgets packaged at all, so let's do the
same without requiring it...

This reverts commit e5405f0ae9.

(cherry picked from commit c87757a913)
2018-06-21 22:23:42 +02:00
Florian Bruhin
b4de889df9 Update changelog 2018-06-21 21:45:43 +02:00
Florian Bruhin
4c9360237f Fix XSS issue on qute://history
Fixes #4011

(cherry picked from commit 5a7869f2fe)
2018-06-21 21:43:53 +02:00
Florian Bruhin
10538738e0 Don't depend on PyQt5.QtQuickWidgets to get RWHV
Some distributions (at least FreeBSD) don't package that module, so let's not
rely on it.

(cherry picked from commit 62d8b5b574)
2018-06-21 21:43:47 +02:00
Florian Bruhin
bd5f84bddf Always clear searches between page loads
Looks like this wasn't properly fixed in Qt for some reason.
Fixes #3693
See #2728 and bef372e5f5

(cherry picked from commit 3399f2df96)
2018-06-21 21:43:30 +02:00
Florian Bruhin
93ae6ad592 Properly add QtQuickWidgets dependency
(cherry picked from commit e5405f0ae9)
2018-06-21 00:22:36 +02:00
Florian Bruhin
146f8e72ae Handle multiple visible children when finding lost focusProxy
When we click a QTBUG link (to open in a new tab) from Qt's codereview, we get
two RWHV objects which both are visible.

Experimenting with .setEnabled(False) it looks like it's (hopefully always...)
the last one which is the one to use.

(cherry picked from commit 67c67db230)
2018-06-11 21:45:09 +02:00
Florian Bruhin
f61662fa52 Only consider visible render widgets for lost focusProxy
Otherwise, when commenting out the focusProxy way above, and using "foo !npm"
with DuckDuckGo, we get two children (one visible, one invisible).

(cherry picked from commit b63e06561d)
2018-06-11 14:02:26 +02:00
Florian Bruhin
e9b4c2a66e Release v1.3.2 2018-06-10 15:58:54 +02:00
Florian Bruhin
6a7ab7edb3 Remove unused import
(cherry picked from commit 7949335a2b)
2018-06-09 21:45:13 +02:00
Florian Bruhin
f7f96484e8 Fix waiting for initial focus object with Qt 5.11 workarounds
This was broken in d32d541ac0 because now
apparently PyQt knows it's a QQuickWidget.

(cherry picked from commit ec88c15390)
2018-06-09 20:11:00 +02:00
Florian Bruhin
840d2e4423 Further simplify getting focusProxy children
(cherry picked from commit d32d541ac0)
2018-06-08 17:11:33 +02:00
Florian Bruhin
e54f2a090a Improve RWHV typecheck for focusProxy
(cherry picked from commit cc497bf2ea)
2018-06-08 15:20:33 +02:00
Florian Bruhin
55ce4b7ed2 Exclude QMenu when trying to find the missing focusProxy
(cherry picked from commit 9725d9ce33)
2018-06-08 15:20:31 +02:00
Florian Bruhin
3daf823da8 Show children in focusProxy workaround
(cherry picked from commit 1531961aeb)
2018-06-08 15:20:29 +02:00
Florian Bruhin
72feb2c19f Fix check for reloads on Qt < 5.11
Equivalent commit on master: 91b4106dcf
This was broken in 900efe4a36
2018-06-08 08:57:26 +02:00
Florian Bruhin
6d04508490 Remove unused import
(cherry picked from commit 4614ad5063)
2018-06-07 18:02:11 +02:00
Florian Bruhin
93ebd846ab Implement a better workaround for chrome-error:// URLs
It looks like chrome-error://chromewebdata/ triggers another invalid scheme
load which is why the endless loop happens. When we install a custom scheme
handler for chrome-error:// we can at least show an error page.

(cherry picked from commit b1506274c5)
2018-06-07 16:04:37 +02:00
Florian Bruhin
9ee473a54c Go back to using an invalid scheme for invalid_link.html
Otherwise, this breaks the tests on Qt 5.10

(cherry picked from commit 596041c40e)
2018-06-07 15:42:02 +02:00
Florian Bruhin
d1cced0da4 Make sure external schemes are clickable via hints
This issue was probably introduced in 545539f28d
- with JavaScript, we can't "click" on an external link.

There might be a better solution using
QWebEngineSettings::setUnknownUrlSchemePolicy(QWebEngineSettings::AllowAllUnknownUrlSchemes)
temporarily when using hints with PyQt 5.11.

Fixes #2833

(cherry picked from commit 89f4333df1)
2018-06-07 14:36:12 +02:00
Florian Bruhin
22644c41da Add a workaround for chrome-error:// loops on Qt 5.11
See #3661

(cherry picked from commit 0c0d204fd4)
2018-06-07 14:36:10 +02:00
Florian Bruhin
6b5857ef7d Skip invalid link tests on Qt 5.11
Qt 5.11 just loads about:blank and doesn't let us catch this in
acceptNavigationRequest, but the same happens in Chromium as well.

See #3661

(cherry picked from commit 999513d5d8)
2018-06-07 14:36:07 +02:00
Florian Bruhin
b0f4cc6924 Use a valid scheme in invalid_link.html
This is to avoid triggering QTBUG-63378 which fails differently with a custom
scheme.

See #3661

(cherry picked from commit d059197bc9)
2018-06-07 14:36:03 +02:00
Florian Bruhin
218637af84 Reenable Qt 5.11 tests on Travis
Fixes #3661

(cherry picked from commit eb6478dd3e)
2018-06-07 14:35:56 +02:00
Florian Bruhin
dd84845f01 Don't run test with failed download on Qt 5.11
Looks like we can't use an <a> tag with download-attribute to trigger a failed
download in the test on Qt 5.11...

See #2298, #3661

(cherry picked from commit 8cc3804119)
2018-06-06 21:14:20 +02:00
Florian Bruhin
cf53f9042a Only set PseudoLayout with Qt 5.11
(cherry picked from commit 456fdc55cc)
2018-06-06 20:30:42 +02:00
Florian Bruhin
f48266f72f Fix lint
(cherry picked from commit 7e31897dcc)
2018-06-06 20:30:39 +02:00
Florian Bruhin
5dac848968 Handle resizing via PseudoLayout
This fixes the scenario where we just get a grey view when opening a link in a
tab from DuckDuckGo.

(cherry picked from commit 5147fc832c)
2018-06-06 20:30:37 +02:00
Florian Bruhin
341aa1e700 Try harder to get the RenderWidgetHostViewQt
(cherry picked from commit ec6c5ebb69)
2018-06-06 20:30:34 +02:00
Florian Bruhin
ebf81c06ae Initial proof of concept for pseudo layout
Fixes #3920 - hopefully properly this time...

(cherry picked from commit cee88cd7ca)
2018-06-06 20:30:31 +02:00
Florian Bruhin
a3eb8d6561 travis: Allow Archlinux to fail for now
See #3661

(cherry picked from commit 6fc3546923)
2018-05-29 13:19:36 +02:00
Florian Bruhin
af4b02bf46 setup.py: Set long_description_content_type
Otherwise, Warehouse (new PyPI) refuses the upload...

(cherry picked from commit 52c44d3da6)
2018-05-29 11:23:48 +02:00
Florian Bruhin
ac29c579ff Release v1.3.1 2018-05-29 11:14:01 +02:00
Florian Bruhin
db5ec363cd Update changelog for v1.3.1 2018-05-29 11:12:33 +02:00
Florian Bruhin
f352c72d1d Fix lint
(cherry picked from commit 12e0edbcd0)
2018-05-28 07:44:49 +02:00
Florian Bruhin
b1f1a0cafa Add some more logging for #3920
(cherry picked from commit 17cfb0d39c)
2018-05-28 07:44:44 +02:00
Florian Bruhin
749056ff90 Use functools instead of a lambda for QTimer
It reads nicer, and this is also speculative fix for #3896 as PyQt5 is
hopefully better at disconnecting partial-objects from dead objects than it is
with lambdas.

(cherry picked from commit 7162f15348)
2018-05-28 07:44:36 +02:00
Florian Bruhin
e7b00ace73 Handle ² keypress correctly
Turns out str.isdigit() also handles ² as a digit, but int('²') causes a
ValueError.

Here we use `string.digits` instead, which is '0123456789'.

Fixes #3743

(cherry picked from commit 29ad252278)
2018-05-22 12:29:09 +02:00
Florian Bruhin
442bdd4a4f Properly work around Qt 5.11 keyboard focus issues
Please let this be the last attempt... :D

Fixes #3939
Supersedes #3921
Reverts ae295a7f65
See #3661

This should not regress #3872. Might affect #3834 in some way.

(cherry picked from commit 71ad8bdb47)
2018-05-22 09:41:13 +02:00
Florian Bruhin
900efe4a36 Fix reload for JavaScript support on Qt 5.11
(cherry picked from commit 6ccd69dad2)
2018-05-17 17:54:39 +02:00
Florian Bruhin
f8a78a0962 Add workaround for the "split page" Qt bug (QTBUG-68224)
Fixes #3920

(cherry picked from commit 44d26f77a5)
2018-05-17 14:25:38 +02:00
Florian Bruhin
60e8abaa89 Improve configuration docs
(cherry picked from commit 20efaeff19)
2018-05-17 14:25:33 +02:00
Florian Bruhin
7762017f00 Release v1.3.0 2018-05-03 19:25:04 +02:00
Florian Bruhin
2ba40acf74 Update changelog for v1.3.0 2018-05-03 19:25:04 +02:00
Florian Bruhin
388c155ebb Add test for #3753 2018-05-03 18:02:20 +02:00
Florian Bruhin
adf2f9860d Disable spellcheck when it's unneeded.
Fixes #3753
2018-05-03 17:58:56 +02:00
Florian Bruhin
f528a5dd21 Update changelog 2018-05-03 17:56:24 +02:00
Florian Bruhin
f0f1a4a1d1 Merge remote-tracking branch 'origin/pr/3702' 2018-05-03 17:56:03 +02:00
Florian Bruhin
ae295a7f65 Call setFocus() when navigating
See #3661:
https://github.com/qutebrowser/qutebrowser/issues/3661#issuecomment-386308601
This doesn't seem to fully fix this, but at least the top four failed tests
there...

This should not regress #3872. Might affect #3834 in some way.
2018-05-03 17:43:19 +02:00
Florian Bruhin
68794cc2e2 Revert "Always set FocusOnNavigationEnabled"
This reverts commit fa41af63b6.

See #3661
Fixes #3872
2018-05-03 15:25:41 +02:00
Florian Bruhin
acdf0a1c60 Call _handle_search before leaving the mode 2018-05-03 15:23:45 +02:00
Florian Bruhin
626abd3c83 Fix lint 2018-05-03 15:18:21 +02:00
Florian Bruhin
979b7cfaba Add a stop-gap solution for AssertionError when retrying downloads
See #3847
2018-05-03 14:52:10 +02:00
Florian Bruhin
2b6b4e82a7 Handle event_target() being None
As a stop-gap solution for #3888
2018-05-03 14:45:55 +02:00
Florian Bruhin
80778a9ff3 Update changelog 2018-05-03 14:04:56 +02:00
Florian Bruhin
6eb8284fe0 Refactor handling search in command.py 2018-05-03 14:04:56 +02:00
Florian Bruhin
49bdcd5a97 Merge remote-tracking branch 'origin/pr/3796' 2018-05-03 13:58:26 +02:00
Florian Bruhin
106e591a36 Refactor matching of Greasemonkey scripts 2018-05-03 13:53:10 +02:00
Florian Bruhin
a70f864ff5 Update changelog 2018-05-03 13:33:53 +02:00
Florian Bruhin
4932cc4d24 Merge remote-tracking branch 'origin/pr/3804' 2018-05-03 13:33:08 +02:00
Florian Bruhin
2b5f133726 Update changelog 2018-05-03 13:23:30 +02:00
Florian Bruhin
5436e27e41 Merge remote-tracking branch 'origin/pr/3756' 2018-05-03 13:21:45 +02:00
Florian Bruhin
4adf10a2f2 Update changelog 2018-05-03 09:13:48 +02:00
Florian Bruhin
b80fa7a197 Merge remote-tracking branch 'origin/pr/3858' 2018-05-03 09:13:31 +02:00
Florian Bruhin
3cc790afb3 Update changelog 2018-05-03 09:10:14 +02:00
Florian Bruhin
91aa9f6c0c Merge remote-tracking branch 'origin/pr/3884' 2018-05-03 09:09:27 +02:00
Florian Bruhin
d6cacdb42f Merge pull request #3876 from qutebrowser/pyup-scheduled-update-2018-04-30
Scheduled weekly dependency update for week 17
2018-05-02 16:22:25 +02:00
Jimmy
19554ba4a1 Update PyPI api URL.
Flask 1.0 is out, pip made breaking changes, warehouse is a thing, new
requests soon. So
much fun in python world lately.
2018-05-02 23:08:51 +12:00
toofar
d16d9e403a Make HTTPClient follow redirects by default.
Closes #3875

The autoupdator, which uses `qutebrowser.misc.httpclient` has been failing recently because the URL that it hits to check version information is now serving a 301 moved permanently. By default QNetworkRequest doesn't follow redirects so it was getting back a (non-json, despite the request) body pointing to the new location, instead or version information. This changes fixes that by changing HTTPClient to use a QNetworkRequest subclass which follows redirects by default.

It lookes like HTTPClient is currently only used in autoupdate.py, version.py, and crashdialog.py so I don't expect any breakage.

5.6-5.8 Only had a boolean setting available which allows redirects, but not from the https scheme to http, 5.9 introduces a more nuanced setting. I have tested locally on 5.7.1 and 5.10.
2018-05-02 23:08:51 +12:00
pyup-bot
432d666d25 Update pytest from 3.5.0 to 3.5.1 2018-04-30 18:29:17 +02:00
pyup-bot
1c3ee0db20 Update flask from 0.12.2 to 1.0.1 2018-04-30 18:29:16 +02:00
pyup-bot
bdc0c0ddc1 Update setuptools from 39.0.1 to 39.1.0 2018-04-30 18:29:14 +02:00
pyup-bot
b9c8a79f10 Update flake8-builtins from 1.3.0 to 1.3.1 2018-04-30 18:29:13 +02:00
cryzed
801e9e0334 qute-pass: Improve fake_key_raw() 2018-04-29 15:22:52 +02:00
Florian Bruhin
cfa5ee2835 Merge pull request #3862 from qutebrowser/pyup-scheduled-update-2018-04-23
Scheduled weekly dependency update for week 16
2018-04-24 11:32:56 +02:00
Florian Bruhin
9c6437b3b9 Update comment 2018-04-24 09:53:41 +02:00
Florian Bruhin
486488e2cd Filter pycodestyle 2.4.0 for pyup
See https://github.com/PyCQA/pycodestyle/issues/741
2018-04-24 09:48:47 +02:00
cryzed
92aedf84f5 qute-pass: Don't use f-strings 2018-04-23 19:16:51 +02:00
cryzed
6825dfb8d8 qute-pass: Fake strings letter-by-letter to avoid issues 2018-04-23 19:01:12 +02:00
pyup-bot
d6c6014b85 Update pytest-mock from 1.8.0 to 1.9.0 2018-04-23 18:24:29 +02:00
pyup-bot
c1ac1d702f Update hypothesis from 3.55.1 to 3.56.5 2018-04-23 18:24:27 +02:00
pyup-bot
9e50b7afcc Update cheroot from 6.1.2 to 6.2.4 2018-04-23 18:24:25 +02:00
pyup-bot
1388880e7b Update github3.py from 1.0.2 to 1.1.0 2018-04-23 18:24:24 +02:00
pyup-bot
30d60ea740 Update github3.py from 1.0.2 to 1.1.0 2018-04-23 18:24:22 +02:00
pyup-bot
28cac01a1f Update pycodestyle from 2.3.1 to 2.4.0 2018-04-23 18:24:21 +02:00
pyup-bot
1689cb09f8 Update flake8-builtins from 1.2.2 to 1.3.0 2018-04-23 18:24:19 +02:00
pyup-bot
286c71a48a Update certifi from 2018.1.18 to 2018.4.16 2018-04-23 18:24:18 +02:00
pyup-bot
c073234a8d Update certifi from 2018.1.18 to 2018.4.16 2018-04-23 18:24:16 +02:00
pyup-bot
8c286412cb Update certifi from 2018.1.18 to 2018.4.16 2018-04-23 18:24:15 +02:00
pyup-bot
b3cef948b0 Update check-manifest from 0.36 to 0.37 2018-04-23 18:24:13 +02:00
Michal Siedlaczek
c94ea5f8d4 Merge remote-tracking branch 'upstream/master' into filter-dict-names
Merging to investigate failed tests that seem unrelated to the PR.
2018-04-21 13:29:18 -04:00
Michal Siedlaczek
e2d249541d Fix test function comment 2018-04-21 12:33:10 -04:00
cryzed
2de6428830 qute-pass: Also escape backslashes in the username 2018-04-20 18:23:50 +02:00
cryzed
c2472d88f1 qute-pass: Escape backslashes, so that they are inserted correctly 2018-04-20 18:21:55 +02:00
Jay Kamat
1d2dd5bf55 Use CommandDispatcher directly for / searches 2018-04-19 21:16:33 -04:00
Jay Kamat
cbb246fd0b Update tests for new implementation 2018-04-16 23:28:32 -04:00
Jay Kamat
646e92707a Call search command directly instead of using arguments 2018-04-16 23:15:56 -04:00
Jimmy
c5334fb683 Greasemonkey: use UrlPatterns for match directives
The greasemonkey `@match` directive is used to match urls against
chromium url patterns (as opposed to `@include` which treats its
argument as a glob expression). I was using fnmatch for both here
because I am lazy and knew someone else was going to implement chromium
url patterns for me eventually. Now it is done and I should switch to
using them instead. The most common failing case that this will fix is
something matching on `*://*.domain.com/*` because it wouldn't match
the url with no subdomain.

This codepath is only used on webengine 5.7.1 and webkit backends.
2018-04-14 10:31:20 +12:00
Jay Kamat
76dbfa7305 Allow searching for double semicolons
Possibly breaks scripts using :search with ;; to split commands. A
workaround is to put the :search command at the end.
2018-04-05 17:20:50 -04:00
Jay Kamat
9ad6cef369 Add a test for leading arguments 2018-04-01 21:00:02 -04:00
Jay Kamat
423192e9c9 Join text arguments for :search 2018-04-01 21:00:02 -04:00
Michal Siedlaczek
d606cd5550 spell test formatting and docstrings 2018-03-28 14:13:46 -04:00
Michal Siedlaczek
7c1de99876 Fix test coverage 2018-03-28 12:16:50 -04:00
Jay Kamat
b873cfb18a Fix style issues in qute-keepass 2018-03-27 23:43:40 -04:00
Jay Kamat
a1776087e0 Fix login when only one entry is available 2018-03-22 21:35:00 -04:00
Jay Kamat
948866f4f2 Add support for keepass keyfiles 2018-03-22 21:21:59 -04:00
Jay Kamat
a9a7f5da45 Fix choking on passwords with <x> syntax in them 2018-03-22 03:01:50 -04:00
Jay Kamat
b169a1c802 Add raw first draft of qute-keepass
This needs a lot more work...
2018-03-22 02:43:15 -04:00
Michal Siedlaczek
f9e702bae5 Warn about malformed dictionaries 2018-03-16 11:28:45 -04:00
Michal Siedlaczek
29eadf7141 Filter installed dictionaries using a regex to ensure correct name 2018-03-11 17:50:20 -04:00
50 changed files with 876 additions and 210 deletions

View File

@@ -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,

View File

@@ -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
------

View File

@@ -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.

View File

@@ -1,3 +1,3 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
check-manifest==0.36
check-manifest==0.37

View File

@@ -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

View File

@@ -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

View File

@@ -15,3 +15,6 @@ flake8-tuple
pep8-naming
pydocstyle
pyflakes
# https://github.com/PyCQA/pycodestyle/issues/741
#@ filter: pycodestyle < 2.4.0

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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))

View File

@@ -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

View File

@@ -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."

View File

@@ -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)

View File

@@ -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):

View File

@@ -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

View File

@@ -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')

View File

@@ -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):

View File

@@ -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):

View File

@@ -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()

View File

@@ -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),

View File

@@ -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()))

View File

@@ -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):

View File

@@ -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()

View File

@@ -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()

View File

@@ -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:

View File

@@ -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">

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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."""

View File

@@ -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.

View File

@@ -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__))

View File

@@ -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'),

View File

@@ -7,5 +7,6 @@
</head>
<body>
<a href="what://::">I'm broken</a>
<a href="foo://bar">Unknown scheme</a>
</body>
</html>

View File

@@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>&lt;img src=&quot;x&quot; onerror=&quot;console.log('XSS')&quot;&gt;foo</title>
</head>
<body>
foo
</body>
</html>

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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()

View File

@@ -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

View File

@@ -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,

View File

@@ -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):