Merge branch 'feature/6218-pick-directory' of github.com:AckslD/qutebrowser into feature/6218-pick-directory

This commit is contained in:
Axel Dahlberg 2021-03-21 12:33:21 +01:00
commit dd3989aa68
133 changed files with 3212 additions and 1418 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2.0.2
current_version = 2.1.0
commit = True
message = Release v{new_version}
tag = True

View File

@ -15,10 +15,102 @@ 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.
[[v2.1.0]]
v2.1.0 (unreleased)
[[v2.2.0]]
v2.2.0 (unreleased)
-------------------
Deprecated
~~~~~~~~~~
- Running qutebrowser with Qt 5.12.0 is now unsupported and logs a warning. It
should still work, however, a workaround for issues with the Nvidia graphic
driver was dropped. Newer Qt 5.12.x versions are still fully supported.
Added
~~~~~
- New `input.media_keys` setting which can be used to disable Chromium's
handling of media keys.
Changed
~~~~~~~
- The `content.ssl_strict` setting got renamed to
`content.tls.certificate_errors`, with new values:
* `ask`: Prompt on overridable certificate errors (`ssl_strict = 'ask'`)
* `ask-block-thirdparty`: See below
* `block`: Block the page load (`ssl_strict = True`)
* `load-insecurely`: Load the page despite the error (`ssl_strict = False`)
- The new `content.tls.certificate_errors` setting now also understands the
value `ask-block-thirdparty`, which asks for page loads but automatically blocks
resource loads on TLS errors. This behavior is consistent with what other
browsers do.
- The prompt text shown on certificate errors has been improved to make it
clearer what kind of error occurred exactly.
- The completion now also shows bindings starting with `set-cmd-text` in its
third column, such as `o` for `:open`.
[[v2.1.1]]
v2.1.1 (unreleased)
-------------------
Added
~~~~~
- Site-specific quirk for krunker.io, which shows a "Socket Error" with
qutebrowser's default Accept-Language header. The workaround is equivalent to
doing `:set -u matchmaker.krunker.io content.headers.accept_language ""`.
Changed
~~~~~~~
- Clicking the 'x' in the devtools window to hide it now also leaves insert
mode.
Fixed
~~~~~
- The workaround for black on (almost) black formula images in dark mode now
also works with Qt 5.12 and 5.13.
- When running in Flatpak, the QtWebEngine version is now detected properly.
Before, a wrong version was assumed, breaking dark mode and certain workarounds
(resulting in crashes on websites like LinkedIn or TradingView).
- When running in Flatpak, communicating with an existing instance now works
properly. Before, a new instance was always opened.
- When the metainfo in the completion database doesn't have the expected
structure, qutebrowser now tries to gracefully recover from the situation
instead of crashing.
- When qutebrowser displays an error during initialization, opening a second
instance would lead to a crash. Instead, qutebrowser now ignores the attempt
to open a new page as long as it's not fully initialized yet.
- When the Brave adblock cache folder was unreadable, qutebrowser crashed. It
now displays an error instead.
- The `qute-pass` userscript now works correctly when generating OTP tokens
with `gopass`.
- When using `bindings.key_mappings` to map a key to multiple other keys,
qutebrowser would crash. This is now handled correctly - however, note that
it's usually better to map keys to commands instead.
[[v2.1.0]]
v2.1.0 (2021-03-12)
-------------------
Removed
~~~~~~~
- The following command aliases were deprecated in v2.0.0 and are now removed:
* `run-macro` -> `macro-run`
* `record-macro` -> `macro-record`
* `buffer` -> `tab-select`
* `open-editor` -> `edit-text`
* `toggle-selection` -> `selection-toggle`
* `drop-selection` -> `selection-drop`
* `reverse-selection` -> `selection-reverse`
* `follow-selected` -> `selection-follow`
* `follow-hint` -> `hint-follow`
* `enter-mode` -> `mode-enter`
* `leave-mode` -> `mode-leave`
Added
~~~~~
@ -32,7 +124,7 @@ Added
Changed
~~~~~~~
- Initial support for Qt 5.15.3 and PyQt 5.15.3
- Initial support for QtWebEngine 5.15.3 and PyQt 5.15.3/.4
- The `colors.webpage.prefers_color_scheme_dark` setting got renamed to
`colors.webpage.preferred_color_scheme` and now takes the values `auto`, `light`
and `dark` (instead of being `True` for dark and `False` for auto).
@ -60,13 +152,25 @@ Changed
- The `fileselect.*.command` settings now support file selectors writing the
selected paths to stdout, which is used if no `{}` placeholder is contained in
the configured command.
- The `--debug-flag` argument now understands a new `log-sensitive-keys` value
which logs all keypresses (including those in insert/passthrough/prompt/...
mode) for debugging.
- The `readability` and `readability-js` userscripts now add a
`qute-readability` CSS class to the page, so that it can be styled easily via
a user stylesheet.
Fixed
~~~~~
- With QtWebEngine 5.15.3 and some locales, Chromium can't start its
subprocesses. As a result, qutebrowser only shows a blank page and logs
"Network service crashed, restarting service.". This release adds a
`qt.workarounds.locale` setting working around the issue. It is disabled by
default since distributions shipping 5.15.3 will probably have a proper patch
for it backported very soon.
- The `colors.webpage.preferred_color_scheme` and `colors.webpage.darkmode.*`
settings now work correctly with the upcoming QtWebEngine 5.15.3 (and Gentoo,
which at the time of writing packages 5.15.3 disguised as 5.15.2).
settings now work correctly with QtWebEngine 5.15.3 (and Gentoo, which at the
time of writing packages 5.15.3 disguised as 5.15.2).
- When dark mode settings were set, existing `blink-features` arguments in
`qt.args` (or `--qt-flag`) were overridden. They are now combined properly.
- On QtWebEngine 5.15.2, auto detection for the `prefers-color-scheme` media
@ -87,6 +191,23 @@ Fixed
properly.
- The "try again" button on error pages now works correctly with JavaScript
disabled.
- If a GreaseMonkey script doesn't have a "@run-at" comment, qutebrowser
accidentally treated that as "@run-at document-idle". However, other
GreaseMonkey implementations default to "@run-at document-end" instead, which
is what qutebrowser now does, too.
- The `hist_importer.py` script didn't work correctly after qutebrowser v2.0.0
and resulted in a history database qutebrowser couldn't read properly. It now
works properly again.
- With certain QtWebEngine versions (5.15.0 based on Chromium 80 and 5.15.3
based on Chromium 87), Chromium's dark mode doesn't invert certain SVG images,
even with `colors.wegpage.darkmode.policy.images` set to `smart`.
Most notably, this causes formulae on Wikipedia to display black on (almost)
black. If `content.site_specific_quirks` is enabled, qutebrowser now injects
some CSS as a workaround, which inverts all math formula images on Wikipedia
(and potentially other sites, if they use the same CSS class).
- When a hint label text started with an apostrophe, it would show an escaped
text until the hints first character has been pressed. It now shows up
correctly.
[[v2.0.2]]
v2.0.2 (2021-02-04)

View File

@ -16,7 +16,7 @@
|<<backend,backend>>|Backend to use to display websites.
|<<bindings.commands,bindings.commands>>|Keybindings mapping keys to commands in different modes.
|<<bindings.default,bindings.default>>|Default keybindings. If you want to add bindings, modify `bindings.commands` instead.
|<<bindings.key_mappings,bindings.key_mappings>>|This setting can be used to map keys to other keys.
|<<bindings.key_mappings,bindings.key_mappings>>|Map keys to other keys, so that they are equivalent in all modes.
|<<changelog_after_upgrade,changelog_after_upgrade>>|When to show a changelog after qutebrowser was upgraded.
|<<colors.completion.category.bg,colors.completion.category.bg>>|Background color of the completion widget category headers.
|<<colors.completion.category.border.bottom,colors.completion.category.border.bottom>>|Bottom border color of the completion widget category headers.
@ -191,7 +191,7 @@
|<<content.proxy_dns_requests,content.proxy_dns_requests>>|Send DNS requests over the configured proxy.
|<<content.register_protocol_handler,content.register_protocol_handler>>|Allow websites to register protocol handlers via `navigator.registerProtocolHandler`.
|<<content.site_specific_quirks,content.site_specific_quirks>>|Enable quirks (such as faked user agent headers) needed to get specific sites to work properly.
|<<content.ssl_strict,content.ssl_strict>>|Validate SSL handshakes.
|<<content.tls.certificate_errors,content.tls.certificate_errors>>|How to proceed on TLS certificate errors.
|<<content.unknown_url_scheme_policy,content.unknown_url_scheme_policy>>|How navigation requests to URLs with unknown schemes are handled.
|<<content.user_stylesheets,content.user_stylesheets>>|List of user stylesheet filenames to use.
|<<content.webgl,content.webgl>>|Enable WebGL.
@ -261,6 +261,7 @@
|<<input.insert_mode.leave_on_load,input.insert_mode.leave_on_load>>|Leave insert mode when starting a new page load.
|<<input.insert_mode.plugins,input.insert_mode.plugins>>|Switch to insert mode when clicking flash and other plugins.
|<<input.links_included_in_focus_chain,input.links_included_in_focus_chain>>|Include hyperlinks in the keyboard focus chain when tabbing.
|<<input.media_keys,input.media_keys>>|Whether the underlying Chromium should handle media keys.
|<<input.mouse.back_forward_buttons,input.mouse.back_forward_buttons>>|Enable back and forward buttons on the mouse.
|<<input.mouse.rocker_gestures,input.mouse.rocker_gestures>>|Enable Opera-like mouse rocker gestures.
|<<input.partial_timeout,input.partial_timeout>>|Timeout (in milliseconds) for partially typed key bindings.
@ -283,6 +284,7 @@
|<<qt.highdpi,qt.highdpi>>|Turn on Qt HighDPI scaling.
|<<qt.low_end_device_mode,qt.low_end_device_mode>>|When to use Chromium's low-end device mode.
|<<qt.process_model,qt.process_model>>|Which Chromium process model to use.
|<<qt.workarounds.locale,qt.workarounds.locale>>|Work around locale parsing issues in QtWebEngine 5.15.3.
|<<qt.workarounds.remove_service_workers,qt.workarounds.remove_service_workers>>|Delete the QtWebEngine Service Worker directory on every start.
|<<scrolling.bar,scrolling.bar>>|When/how to show the scrollbar.
|<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages.
@ -773,9 +775,10 @@ Default:
[[bindings.key_mappings]]
=== bindings.key_mappings
This setting can be used to map keys to other keys.
Map keys to other keys, so that they are equivalent in all modes.
When the key used as dictionary-key is pressed, the binding for the key used as dictionary-value is invoked instead.
This is useful for global remappings of keys, for example to map Ctrl-[ to Escape.
This is useful for global remappings of keys, for example to map <Ctrl-[> to <Escape>.
NOTE: This should only be used if two keys should always be equivalent, i.e. for things like <Enter> (keypad) and <Return> (non-keypad). For normal command bindings, qutebrowser works differently to vim: You always bind keys to commands, usually via `:bind` or `config.bind()`. Instead of using this setting, consider finding the command a key is bound to (e.g. via `:bind gg`) and then binding the same command to the desired key.
Note that when a key is bound (via `bindings.default` or `bindings.commands`), the mapping is ignored.
Type: <<types,Dict>>
@ -1599,6 +1602,8 @@ The `lightness-cielab` value was added with QtWebEngine 5.14 and is treated like
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
Valid values:
@ -1609,8 +1614,6 @@ Valid values:
Default: +pass:[lightness-cielab]+
This setting is only available with the QtWebEngine backend.
[[colors.webpage.darkmode.contrast]]
=== colors.webpage.darkmode.contrast
Contrast for dark mode.
@ -1618,12 +1621,12 @@ This only has an effect when `colors.webpage.darkmode.algorithm` is set to `ligh
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,Float>>
Default: +pass:[0.0]+
This setting is only available with the QtWebEngine backend.
[[colors.webpage.darkmode.enabled]]
=== colors.webpage.darkmode.enabled
Render all web contents using a dark theme.
@ -1644,12 +1647,12 @@ Example configurations from Chromium's `chrome://flags`:
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
Default: +pass:[false]+
This setting is only available with the QtWebEngine backend.
[[colors.webpage.darkmode.grayscale.all]]
=== colors.webpage.darkmode.grayscale.all
Render all colors as grayscale.
@ -1657,12 +1660,12 @@ This only has an effect when `colors.webpage.darkmode.algorithm` is set to `ligh
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
Default: +pass:[false]+
This setting is only available with the QtWebEngine backend.
[[colors.webpage.darkmode.grayscale.images]]
=== colors.webpage.darkmode.grayscale.images
Desaturation factor for images in dark mode.
@ -1670,14 +1673,14 @@ If set to 0, images are left as-is. If set to 1, images are completely grayscale
This setting requires a restart.
Type: <<types,Float>>
Default: +pass:[0.0]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
Type: <<types,Float>>
Default: +pass:[0.0]+
[[colors.webpage.darkmode.policy.images]]
=== colors.webpage.darkmode.policy.images
Which images to apply dark mode to.
@ -1685,6 +1688,8 @@ With QtWebEngine 5.15.0, this setting can cause frequent renderer process crashe
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
Valid values:
@ -1695,8 +1700,6 @@ Valid values:
Default: +pass:[smart]+
This setting is only available with the QtWebEngine backend.
[[colors.webpage.darkmode.policy.page]]
=== colors.webpage.darkmode.policy.page
Which pages to apply dark mode to.
@ -1704,6 +1707,10 @@ The underlying Chromium setting has been removed in QtWebEngine 5.15.3, thus thi
This setting requires a restart.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
Type: <<types,String>>
Valid values:
@ -1713,10 +1720,6 @@ Valid values:
Default: +pass:[smart]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[colors.webpage.darkmode.threshold.background]]
=== colors.webpage.darkmode.threshold.background
Threshold for inverting background elements with dark mode.
@ -1725,14 +1728,14 @@ Note: This behavior is the opposite of `colors.webpage.darkmode.threshold.text`!
This setting requires a restart.
Type: <<types,Int>>
Default: +pass:[0]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
Type: <<types,Int>>
Default: +pass:[0]+
[[colors.webpage.darkmode.threshold.text]]
=== colors.webpage.darkmode.threshold.text
Threshold for inverting text with dark mode.
@ -1740,14 +1743,14 @@ Text colors with brightness below this threshold will be inverted, and above it
This setting requires a restart.
Type: <<types,Int>>
Default: +pass:[256]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
Type: <<types,Int>>
Default: +pass:[256]+
[[colors.webpage.preferred_color_scheme]]
=== colors.webpage.preferred_color_scheme
Value to use for `prefers-color-scheme:` for websites.
@ -1756,6 +1759,10 @@ The "auto" value is broken on QtWebEngine 5.15.2 due to a Qt bug. There, it will
This setting requires a restart.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
Type: <<types,String>>
Valid values:
@ -1766,10 +1773,6 @@ Valid values:
Default: +pass:[auto]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
[[completion.cmd_history_max_items]]
=== completion.cmd_history_max_items
Number of commands to save in the command history.
@ -1941,12 +1944,12 @@ Automatically start playing `<video>` elements.
This setting supports URL patterns.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
Default: +pass:[true]+
This setting is only available with the QtWebEngine backend.
[[content.blocking.adblock.lists]]
=== content.blocking.adblock.lists
List of URLs to ABP-style adblocking rulesets.
@ -2039,24 +2042,24 @@ An application cache acts like an HTTP cache in some sense. For documents that u
This setting supports URL patterns.
This setting is only available with the QtWebKit backend.
Type: <<types,Bool>>
Default: +pass:[true]+
This setting is only available with the QtWebKit backend.
[[content.cache.maximum_pages]]
=== content.cache.maximum_pages
Maximum number of pages to hold in the global memory page cache.
The page cache allows for a nicer user experience when navigating forth or back to pages in the forward/back history, by pausing and resuming up to _n_ pages.
For more information about the feature, please refer to: https://webkit.org/blog/427/webkit-page-cache-i-the-basics/
This setting is only available with the QtWebKit backend.
Type: <<types,Int>>
Default: +pass:[0]+
This setting is only available with the QtWebKit backend.
[[content.cache.size]]
=== content.cache.size
Size (in bytes) of the HTTP network cache. Null to use the default value.
@ -2073,12 +2076,12 @@ Note this is needed for some websites to work properly.
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
Default: +pass:[true]+
This setting is only available with the QtWebEngine backend.
[[content.cookies.accept]]
=== content.cookies.accept
Which cookies to accept.
@ -2138,12 +2141,12 @@ Try to pre-fetch DNS entries to speed up browsing.
This setting supports URL patterns.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
Default: +pass:[true]+
This setting is only available with the QtWebEngine backend.
[[content.frame_flattening]]
=== content.frame_flattening
Expand each subframe to its contents.
@ -2151,12 +2154,12 @@ This will flatten all the frames to become one scrollable page.
This setting supports URL patterns.
This setting is only available with the QtWebKit backend.
Type: <<types,Bool>>
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
[[content.fullscreen.overlay_timeout]]
=== content.fullscreen.overlay_timeout
Set fullscreen notification overlay timeout in milliseconds.
@ -2315,12 +2318,12 @@ Allow JavaScript to close tabs.
This setting supports URL patterns.
This setting is only available with the QtWebKit backend.
Type: <<types,Bool>>
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
[[content.javascript.can_open_tabs_automatically]]
=== content.javascript.can_open_tabs_automatically
Allow JavaScript to open new tabs without user interaction.
@ -2409,6 +2412,8 @@ Allow websites to record audio.
This setting supports URL patterns.
This setting is only available with the QtWebEngine backend.
Type: <<types,BoolAsk>>
Valid values:
@ -2419,14 +2424,14 @@ Valid values:
Default: +pass:[ask]+
This setting is only available with the QtWebEngine backend.
[[content.media.audio_video_capture]]
=== content.media.audio_video_capture
Allow websites to record audio and video.
This setting supports URL patterns.
This setting is only available with the QtWebEngine backend.
Type: <<types,BoolAsk>>
Valid values:
@ -2437,14 +2442,14 @@ Valid values:
Default: +pass:[ask]+
This setting is only available with the QtWebEngine backend.
[[content.media.video_capture]]
=== content.media.video_capture
Allow websites to record video.
This setting supports URL patterns.
This setting is only available with the QtWebEngine backend.
Type: <<types,BoolAsk>>
Valid values:
@ -2455,14 +2460,14 @@ Valid values:
Default: +pass:[ask]+
This setting is only available with the QtWebEngine backend.
[[content.mouse_lock]]
=== content.mouse_lock
Allow websites to lock your mouse pointer.
This setting supports URL patterns.
This setting is only available with the QtWebEngine backend.
Type: <<types,BoolAsk>>
Valid values:
@ -2473,8 +2478,6 @@ Valid values:
Default: +pass:[ask]+
This setting is only available with the QtWebEngine backend.
[[content.mute]]
=== content.mute
Automatically mute tabs.
@ -2501,6 +2504,8 @@ Allow websites to show notifications.
This setting supports URL patterns.
On QtWebEngine, this setting requires Qt 5.13 or newer.
Type: <<types,BoolAsk>>
Valid values:
@ -2511,8 +2516,6 @@ Valid values:
Default: +pass:[ask]+
On QtWebEngine, this setting requires Qt 5.13 or newer.
[[content.pdfjs]]
=== content.pdfjs
Allow pdf.js to view PDF files in the browser.
@ -2530,6 +2533,8 @@ Allow websites to request persistent storage quota via `navigator.webkitPersiste
This setting supports URL patterns.
This setting is only available with the QtWebEngine backend.
Type: <<types,BoolAsk>>
Valid values:
@ -2540,8 +2545,6 @@ Valid values:
Default: +pass:[ask]+
This setting is only available with the QtWebEngine backend.
[[content.plugins]]
=== content.plugins
Enable plugins in Web pages.
@ -2558,12 +2561,12 @@ Draw the background color and images also when the page is printed.
This setting supports URL patterns.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
Default: +pass:[true]+
This setting is only available with the QtWebEngine backend.
[[content.private_browsing]]
=== content.private_browsing
Open new windows in private browsing mode which does not record visited pages.
@ -2591,18 +2594,20 @@ Default: +pass:[system]+
=== content.proxy_dns_requests
Send DNS requests over the configured proxy.
This setting is only available with the QtWebKit backend.
Type: <<types,Bool>>
Default: +pass:[true]+
This setting is only available with the QtWebKit backend.
[[content.register_protocol_handler]]
=== content.register_protocol_handler
Allow websites to register protocol handlers via `navigator.registerProtocolHandler`.
This setting supports URL patterns.
This setting is only available with the QtWebEngine backend.
Type: <<types,BoolAsk>>
Valid values:
@ -2613,8 +2618,6 @@ Valid values:
Default: +pass:[ask]+
This setting is only available with the QtWebEngine backend.
[[content.site_specific_quirks]]
=== content.site_specific_quirks
Enable quirks (such as faked user agent headers) needed to get specific sites to work properly.
@ -2625,19 +2628,20 @@ Type: <<types,Bool>>
Default: +pass:[true]+
[[content.ssl_strict]]
=== content.ssl_strict
Validate SSL handshakes.
[[content.tls.certificate_errors]]
=== content.tls.certificate_errors
How to proceed on TLS certificate errors.
This setting supports URL patterns.
Type: <<types,BoolAsk>>
Type: <<types,String>>
Valid values:
* +true+
* +false+
* +ask+
* +ask+: Ask how to proceed for every certificate error (unless non-overridable due to HSTS).
* +ask-block-thirdparty+: Ask how to proceed for normal page loads, but silently block resource loads.
* +block+: Automatically block loading on certificate errors.
* +load-insecurely+: Force loading pages despite certificate errors. This is *insecure* and should be avoided. Instead of using this, consider fixing the underlying issue or importing a self-signed certificate via `certutil` (or Chromium) instead.
Default: +pass:[ask]+
@ -2647,6 +2651,8 @@ How navigation requests to URLs with unknown schemes are handled.
This setting supports URL patterns.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
Valid values:
@ -2657,8 +2663,6 @@ Valid values:
Default: +pass:[allow-from-user-interaction]+
This setting is only available with the QtWebEngine backend.
[[content.user_stylesheets]]
=== content.user_stylesheets
List of user stylesheet filenames to use.
@ -2683,6 +2687,8 @@ Which interfaces to expose via WebRTC.
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
Valid values:
@ -2694,8 +2700,6 @@ Valid values:
Default: +pass:[all-interfaces]+
This setting is only available with the QtWebEngine backend.
[[content.xss_auditing]]
=== content.xss_auditing
Monitor load requests for cross-site scripting attempts.
@ -3119,6 +3123,8 @@ Default: +pass:[/usr/share/dict/words]+
=== hints.find_implementation
Which implementation to use to find elements to hint.
This setting is only available with the QtWebKit backend.
Type: <<types,String>>
Valid values:
@ -3128,8 +3134,6 @@ Valid values:
Default: +pass:[python]+
This setting is only available with the QtWebKit backend.
[[hints.hide_unmatched_rapid_hints]]
=== hints.hide_unmatched_rapid_hints
Hide unmatched hints in rapid mode.
@ -3391,6 +3395,21 @@ Type: <<types,Bool>>
Default: +pass:[true]+
[[input.media_keys]]
=== input.media_keys
Whether the underlying Chromium should handle media keys.
On Linux, disabling this also disables Chromium's MPRIS integration.
This setting requires a restart.
On QtWebEngine, this setting requires Qt 5.14 or newer.
On QtWebKit, this setting is unavailable.
Type: <<types,Bool>>
Default: +pass:[true]+
[[input.mouse.back_forward_buttons]]
=== input.mouse.back_forward_buttons
Enable back and forward buttons on the mouse.
@ -3599,6 +3618,8 @@ This is needed for QtWebEngine to work with Nouveau drivers and can be useful in
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
Valid values:
@ -3610,8 +3631,6 @@ Valid values:
Default: +pass:[none]+
This setting is only available with the QtWebEngine backend.
[[qt.highdpi]]
=== qt.highdpi
Turn on Qt HighDPI scaling.
@ -3631,6 +3650,8 @@ This improves the RAM usage of renderer processes, at the expense of performance
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
Valid values:
@ -3641,8 +3662,6 @@ Valid values:
Default: +pass:[auto]+
This setting is only available with the QtWebEngine backend.
[[qt.process_model]]
=== qt.process_model
Which Chromium process model to use.
@ -3654,6 +3673,8 @@ See the following pages for more details:
This setting requires a restart.
This setting is only available with the QtWebEngine backend.
Type: <<types,String>>
Valid values:
@ -3664,8 +3685,18 @@ Valid values:
Default: +pass:[process-per-site-instance]+
[[qt.workarounds.locale]]
=== qt.workarounds.locale
Work around locale parsing issues in QtWebEngine 5.15.3.
With some locales, QtWebEngine 5.15.3 is unusable without this workaround. In affected scenarios, QtWebEngine will log "Network service crashed, restarting service." and only display a blank page.
However, It is expected that distributions shipping QtWebEngine 5.15.3 follow up with a proper fix soon, so it is disabled by default.
This setting is only available with the QtWebEngine backend.
Type: <<types,Bool>>
Default: +pass:[false]+
[[qt.workarounds.remove_service_workers]]
=== qt.workarounds.remove_service_workers
Delete the QtWebEngine Service Worker directory on every start.
@ -3728,12 +3759,12 @@ Default: +pass:[true]+
=== search.wrap
Wrap around at the top and bottom of the page when advancing through text matches using `:search-next` and `:search-prev`.
On QtWebEngine, this setting requires Qt 5.14 or newer.
Type: <<types,Bool>>
Default: +pass:[true]+
On QtWebEngine, this setting requires Qt 5.14 or newer.
[[session.default_name]]
=== session.default_name
Name of the session to save by default.
@ -3756,6 +3787,8 @@ Default: +pass:[false]+
Languages to use for spell checking.
You can check for available languages and install dictionaries using scripts/dictcli.py. Run the script with -h/--help for instructions.
This setting is only available with the QtWebEngine backend.
Type: <<types,List of String>>
Valid values:
@ -3806,8 +3839,6 @@ Valid values:
Default: empty
This setting is only available with the QtWebEngine backend.
[[statusbar.padding]]
=== statusbar.padding
Padding (in pixels) for the statusbar.
@ -4425,12 +4456,12 @@ Apply the zoom factor on a frame only to the text or to all content.
This setting supports URL patterns.
This setting is only available with the QtWebKit backend.
Type: <<types,Bool>>
Default: +pass:[false]+
This setting is only available with the QtWebKit backend.
== Setting types
[[types]]
[options="header",width="75%",cols="25%,75%"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 KiB

After

Width:  |  Height:  |  Size: 763 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@ -239,11 +239,12 @@ qutebrowser is available
https://flathub.org/apps/details/org.qutebrowser.qutebrowser[on Flathub]
as `org.qutebrowser.qutebrowser`.
WARNING: As of October 2020, the Flatpak package is severely outdated (qutebrowser
v1.7.0 from July 2019) and, among other issues, misses fixes for a
(low-severity) https://github.com/qutebrowser/qutebrowser/security/advisories/GHSA-4rcq-jv2f-898j[security issue].
It's recommended to <<tox,install qutebrowser in a virtualenv>> instead, which
is one of the officially maintained options and will always be up-to-date.
NOTE: The Flatpak package is
https://github.com/flathub/org.qutebrowser.qutebrowser/issues/8#issuecomment-799579975[looking for (co-)maintainers].
The package recently was updated after being out of date for multiple years. It
currently (March 2021) is up to date again. If that situation changes, consider
to <<tox,install qutebrowser in a virtualenv>> instead, which is one of the
officially maintained options and will always be up-to-date.
On FreeBSD
----------

View File

@ -11,7 +11,7 @@
height="682.66669"
id="svg2"
sodipodi:version="0.32"
inkscape:version="1.0.1 (3bc2e813f5, 2020-09-07, custom)"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
version="1.0"
sodipodi:docname="cheatsheet.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
@ -30,16 +30,16 @@
objecttolerance="10"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.7536248"
inkscape:cx="466.08451"
inkscape:cy="268.64059"
inkscape:zoom="2.48"
inkscape:cx="834.18001"
inkscape:cy="692.30401"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="1024px"
height="640px"
showgrid="false"
inkscape:window-width="3822"
inkscape:window-height="2128"
inkscape:window-width="1914"
inkscape:window-height="1048"
inkscape:window-x="0"
inkscape:window-y="16"
showguides="true"
@ -3113,8 +3113,6 @@
style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
id="flowPara3925">ss - set setting (sl: temp)</flowPara><flowPara
style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
id="flowPara3927" /><flowPara
style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
id="flowPara3929">sk - bind key</flowPara><flowPara
style="font-size:10.6667px;line-height:1.25;font-family:sans-serif;fill:#000000;stroke-width:1.06667"
id="flowPara3931">Ss - show settings</flowPara><flowPara

Before

Width:  |  Height:  |  Size: 175 KiB

After

Width:  |  Height:  |  Size: 175 KiB

View File

@ -44,6 +44,7 @@
</content_rating>
<releases>
<!-- Add new releases here -->
<release version="2.1.0" date="2021-03-12"/>
<release version="2.0.2" date="2021-02-04"/>
<release version="2.0.1" date="2021-01-28"/>
<release version="2.0.0" date="2021-01-28"/>

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
build==0.3.0
build==0.3.1.post1
check-manifest==0.46
packaging==20.9
pep517==0.9.1
pep517==0.10.0
pyparsing==2.4.7
toml==0.10.2

View File

@ -15,10 +15,10 @@ packaging==20.9
pycparser==2.20
Pympler==0.9
pyparsing==2.4.7
PyQt-builder==1.9.0
PyQt-builder==1.9.1
python-dateutil==2.8.1
requests==2.25.1
sip==6.0.2
sip==6.0.3
six==1.15.0
toml==0.10.2
uritemplate==3.0.1

View File

@ -1,8 +1,8 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
attrs==20.3.0
flake8==3.8.4
flake8-bugbear==20.11.1
flake8==3.9.0
flake8-bugbear==21.3.2
flake8-builtins==1.5.3
flake8-comprehensions==3.3.1
flake8-copyright==0.2.2
@ -17,8 +17,8 @@ flake8-tidy-imports==4.2.1
flake8-tuple==0.4.1
mccabe==0.6.1
pep8-naming==0.11.1
pycodestyle==2.6.0
pycodestyle==2.7.0
pydocstyle==5.1.1
pyflakes==2.2.0
pyflakes==2.3.0
six==1.15.0
snowballstemmer==2.1.0

View File

@ -1,10 +1,10 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
chardet==4.0.0
diff-cover==4.2.1
importlib-metadata==3.7.0
importlib-resources==5.1.1
inflect==5.2.0
diff-cover==5.0.1
importlib-metadata==3.7.3
importlib-resources==5.1.2
inflect==5.3.0
Jinja2==2.11.3
jinja2-pluralize==0.3.0
lxml==4.6.2
@ -12,8 +12,8 @@ MarkupSafe==1.1.1
mypy==0.812
mypy-extensions==0.4.3
pluggy==0.13.1
Pygments==2.8.0
Pygments==2.8.1
PyQt5-stubs==5.15.2.0
typed-ast==1.4.2
typing-extensions==3.7.4.3
zipp==3.4.0
zipp==3.4.1

View File

@ -2,4 +2,4 @@
altgraph==0.17
pyinstaller==4.2
pyinstaller-hooks-contrib==2020.11
pyinstaller-hooks-contrib==2021.1

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.3 # rq.filter: < 5.16
PyQt5-Qt==5.15.2
PyQt5==5.15.4 # rq.filter: < 5.16
PyQt5-Qt5==5.15.2
PyQt5-sip==12.8.1
PyQtWebEngine==5.15.3 # rq.filter: < 5.16
PyQtWebEngine-Qt==5.15.2
PyQtWebEngine==5.15.4 # rq.filter: < 5.16
PyQtWebEngine-Qt5==5.15.2

View File

@ -0,0 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.3
PyQt5-Qt==5.15.2
PyQt5-sip==12.8.1
PyQtWebEngine==5.15.3
PyQtWebEngine-Qt==5.15.2

View File

@ -0,0 +1,2 @@
PyQt5==5.15.3
PyQtWebEngine==5.15.3

View File

@ -1,7 +1,7 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
PyQt5==5.15.3
PyQt5-Qt==5.15.2
PyQt5==5.15.4
PyQt5-Qt5==5.15.2
PyQt5-sip==12.8.1
PyQtWebEngine==5.15.3
PyQtWebEngine-Qt==5.15.2
PyQtWebEngine==5.15.4
PyQtWebEngine-Qt5==5.15.2

View File

@ -1,5 +1,5 @@
# This file is automatically generated by scripts/dev/recompile_requirements.py
docutils==0.16
Pygments==2.5.2
pyroma==2.6.1
Pygments==2.8.1
pyroma==3.1

View File

@ -10,12 +10,12 @@ imagesize==1.2.0
Jinja2==2.11.3
MarkupSafe==1.1.1
packaging==20.9
Pygments==2.8.0
Pygments==2.8.1
pyparsing==2.4.7
pytz==2021.1
requests==2.25.1
snowballstemmer==2.1.0
Sphinx==3.5.1
Sphinx==3.5.2
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==1.0.3

View File

@ -0,0 +1,37 @@
# Problematic: needs bzr
# bzr+lp:beautifulsoup
beautifulsoup4
git+https://github.com/cherrypy/cheroot.git
git+https://github.com/nedbat/coveragepy.git
git+https://github.com/pallets/flask.git
git+https://github.com/pallets/werkzeug.git # transitive dep, but needed to work
git+https://github.com/HypothesisWorks/hypothesis.git#subdirectory=hypothesis-python
git+https://github.com/pytest-dev/pytest.git
git+https://github.com/pytest-dev/pytest-bdd.git
git+https://github.com/ionelmc/pytest-benchmark.git
git+https://github.com/pytest-dev/pytest-instafail.git
git+https://github.com/pytest-dev/pytest-mock.git
git+https://github.com/pytest-dev/pytest-qt.git
git+https://github.com/pytest-dev/pytest-rerunfailures.git
git+https://github.com/ionelmc/python-hunter.git
git+https://github.com/jendrikseipp/vulture.git
git+https://github.com/pygments/pygments.git
git+https://github.com/pytest-dev/pytest-repeat.git
git+https://github.com/pytest-dev/pytest-cov.git
git+https://github.com/The-Compiler/pytest-xvfb.git
git+https://github.com/pytest-dev/pytest-xdist.git
git+https://github.com/hjwp/pytest-icdiff.git
git+https://github.com/john-kurkowski/tldextract
# Problematic: needs rust (and some time to build)
# git+https://github.com/ArniDagur/python-adblock.git
adblock ; python_version!="3.10"
## qutebrowser dependencies
git+https://github.com/pallets/jinja.git
git+https://github.com/yaml/pyyaml.git
git+https://github.com/tartley/colorama.git
# https://github.com/pyparsing/pyparsing/issues/271
pyparsing!=3.0.0b2,!=3.0.0b1

View File

@ -1,34 +0,0 @@
bzr+lp:beautifulsoup
git+https://github.com/cherrypy/cheroot.git
hg+https://bitbucket.org/ned/coveragepy
git+https://github.com/micheles/decorator.git
git+https://github.com/pallets/flask.git
git+https://github.com/miracle2k/python-glob2.git
git+https://github.com/HypothesisWorks/hypothesis-python.git
git+https://github.com/pallets/itsdangerous.git
git+https://bitbucket.org/zzzeek/mako.git
git+https://github.com/r1chardj0n3s/parse.git
git+https://github.com/jenisys/parse_type.git
hg+https://bitbucket.org/pytest-dev/py
git+https://github.com/pytest-dev/pytest.git@features
git+https://github.com/pytest-dev/pytest-bdd.git
git+https://github.com/pytest-dev/pytest-cov.git
git+https://github.com/pytest-dev/pytest-instafail.git
git+https://github.com/pytest-dev/pytest-mock.git
git+https://github.com/pytest-dev/pytest-qt.git
git+https://github.com/pytest-dev/pytest-repeat.git
git+https://github.com/pytest-dev/pytest-rerunfailures.git
git+https://github.com/The-Compiler/pytest-xvfb.git
hg+https://bitbucket.org/gutworth/six
hg+https://bitbucket.org/jendrikseipp/vulture
git+https://github.com/pallets/werkzeug.git
## qutebrowser dependencies
git+https://github.com/tartley/colorama.git
git+https://github.com/pallets/jinja.git
git+https://github.com/pallets/markupsafe.git
git+https://github.com/pygments/pygments.git
git+https://github.com/python-attrs/attrs.git
git+https://github.com/yaml/pyyaml.git

View File

@ -15,7 +15,7 @@ filelock==3.0.12
Flask==1.1.2
glob2==0.7
hunter==3.3.1
hypothesis==6.3.4
hypothesis==6.8.1
icdiff==1.9.1
idna==2.10
iniconfig==1.1.1
@ -33,7 +33,7 @@ pluggy==0.13.1
pprintpp==0.4.0
py==1.10.0
py-cpuinfo==7.0.0
Pygments==2.8.0
Pygments==2.8.1
pyparsing==2.4.7
pytest==6.2.2
pytest-bdd==4.0.2
@ -48,7 +48,7 @@ pytest-repeat==0.9.1
pytest-rerunfailures==9.1.1
pytest-xdist==2.2.1
pytest-xvfb==2.0.0
PyVirtualDisplay==2.0
PyVirtualDisplay==2.1
requests==2.25.1
requests-file==1.5.1
six==1.15.0

View File

@ -8,9 +8,9 @@ pip==21.0.1
pluggy==0.13.1
py==1.10.0
pyparsing==2.4.7
setuptools==54.0.0
setuptools==54.1.2
six==1.15.0
toml==0.10.2
tox==3.22.0
tox==3.23.0
virtualenv==20.4.2
wheel==0.36.2

View File

@ -70,7 +70,7 @@ GPG might then ask for your private-key passwort whenever you query the database
[3]: https://gnupg.org/
[4]: https://github.com/keepassxreboot/keepassxc-browser/blob/develop/keepassxc-protocol.md
[5]: https://github.com/qutebrowser/qutebrowser/blob/master/doc/userscripts.asciidoc
[6]: https://keepassxc.org/docs/keepassxc-browser-migration/
[6]: https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_setup_browser_integration
"""
import sys

View File

@ -60,6 +60,8 @@ argument_parser.add_argument('--io-encoding', '-i', default='UTF-8',
help='Encoding used to communicate with subprocesses')
argument_parser.add_argument('--merge-candidates', '-m', action='store_true',
help='Merge pass candidates for fully-qualified and registered domain name')
argument_parser.add_argument('--no-tld-download', action='store_true',
help="Don't download TLD list")
group = argument_parser.add_mutually_exclusive_group()
group.add_argument('--username-only', '-e',
action='store_true', help='Only insert username')
@ -117,7 +119,12 @@ def main(arguments):
argument_parser.print_help()
return ExitCodes.FAILURE
extract_result = tldextract.extract(arguments.url)
if arguments.no_tld_download:
extract = tldextract.TLDExtract(suffix_list_urls=None)
else:
extract = tldextract.extract
extract_result = extract(arguments.url)
# Try to find candidates using targets in the following order: fully-qualified domain name (includes subdomains),
# the registered domain name and finally: the IPv4 address if that's what

View File

@ -158,6 +158,8 @@ def pass_(path):
def pass_otp(path):
if arguments.mode == "gopass":
return _run_pass(['otp', '-o', path])
return _run_pass(['otp', path])

View File

@ -57,6 +57,9 @@ with codecs.open(os.environ['QUTE_HTML'], 'r', 'utf-8') as source:
title = doc.title()
content = doc.summary().replace('<html>', HEADER % title)
# add a class to make styling the page easier
content = content.replace('<body>', '<body class="qute-readability">')
with codecs.open(tmpfile, 'w', 'utf-8') as target:
target.write(content.lstrip())

View File

@ -39,8 +39,8 @@ const HEADER = `
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, text/html, charset=UTF-8" http-equiv="Content-Type">
</meta>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="UTF-8">
<title>%s</title>
<style type="text/css">
body {
@ -106,7 +106,12 @@ const HEADER = `
SAwLTIgMC44OTUtMiAyczAuODk1IDIgMiAyaDIwYzEuMTEgMCAyLTAuODk1IDItMnMtMC44OTUtMi0yLTJ6bTAgOGgtMjBjLTEuMTEgMC0yIDAuODk1LTIg
MnMwLjg5NSAyIDIgMmgyMGMxLjExIDAgMi0wLjg5NSAyLTJzLTAuODk1LTItMi0yem0tMTIgOGgtOGMtMS4xMSAwLTIgMC44OTUtMiAyczAuODk1IDIgMiA
yaDhjMS4xMSAwIDItMC44OTUgMi0ycy0wLjg5NS0yLTItMnoiIGZpbGw9IiNmZmYiLz4KPC9nPgo8L3N2Zz4K"/>
</head>`;
</head>
<body class="qute-readability">
%s
</body>
</html>
`;
const scriptsDir = path.join(process.env.QUTE_DATA_DIR, 'userscripts');
const tmpFile = path.join(scriptsDir, '/readability.html');
@ -129,7 +134,7 @@ else {
getDOM(target, domOpts).then(dom => {
let reader = new Readability(dom.window.document);
let article = reader.parse();
let content = util.format(HEADER, article.title) + article.content;
let content = util.format(HEADER, article.title, article.content);
fs.writeFile(tmpFile, content, (err) => {
if (err) {

View File

@ -34,7 +34,6 @@ markers =
no_invalid_lines: Don't fail on unparsable lines in end2end tests
fake_os: Fake utils.is_* to a fake operating system
unicode_locale: Tests which need a unicode locale to work
qtwebkit6021_xfail: Tests which would fail on WebKit version 602.1
js_headers: Sets JS headers dynamically on QtWebEngine (unsupported on some versions)
qtwebkit_pdf_imageformat_skip: Broken on QtWebKit with PDF image format plugin installed
windows_skip: Tests which should be skipped on Windows

View File

@ -26,7 +26,7 @@ __copyright__ = "Copyright 2014-2021 Florian Bruhin (The Compiler)"
__license__ = "GPL"
__maintainer__ = __author__
__email__ = "mail@qutebrowser.org"
__version__ = "2.0.2"
__version__ = "2.1.0"
__version_info__ = tuple(int(part) for part in __version__.split('.'))
__description__ = "A keyboard-driven, vim-like browser based on PyQt5."

View File

@ -66,7 +66,8 @@ from qutebrowser.misc import (ipc, savemanager, sessions, crashsignal,
earlyinit, sql, cmdhistory, backendproblem,
objects, quitter)
from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
usertypes, standarddir, error, qtutils, debug)
resources, usertypes, standarddir,
error, qtutils, debug)
# pylint: disable=unused-import
# We import those to run the cmdutils.register decorators.
from qutebrowser.mainwindow.statusbar import command
@ -86,7 +87,7 @@ def run(args):
log.init.debug("Initializing directories...")
standarddir.init(args)
utils.preload_resources()
resources.preload()
log.init.debug("Initializing config...")
configinit.early_init(args)
@ -119,13 +120,14 @@ def run(args):
log.init.warning(
"Backend from the running instance will be used")
sys.exit(usertypes.Exit.ok)
else:
quitter.instance.shutting_down.connect(server.shutdown)
server.got_args.connect(lambda args, target_arg, cwd:
process_pos_args(args, cwd=cwd, via_ipc=True,
target_arg=target_arg))
init(args=args)
quitter.instance.shutting_down.connect(server.shutdown)
server.got_args.connect(
lambda args, target_arg, cwd:
process_pos_args(args, cwd=cwd, via_ipc=True, target_arg=target_arg))
ret = qt_mainloop()
return ret
@ -395,7 +397,7 @@ def _open_special_pages(args):
return
try:
changelog = utils.read_file('html/doc/changelog.html')
changelog = resources.read_file('html/doc/changelog.html')
except OSError as e:
log.init.warning(f"Not showing changelog due to {e}")
return

View File

@ -917,7 +917,7 @@ class CommandDispatcher:
return (tabbed_browser, tabbed_browser.widget.widget(idx-1))
@cmdutils.register(instance='command-dispatcher', scope='window',
maxsplit=0, deprecated_name='buffer')
maxsplit=0)
@cmdutils.argument('index', completion=miscmodels.tabs)
@cmdutils.argument('count', value=cmdutils.Value.count)
def tab_select(self, index=None, count=None):
@ -1488,8 +1488,7 @@ class CommandDispatcher:
objreg.last_focused_window(), alert=False))
ed.edit(text, caret_position)
@cmdutils.register(instance='command-dispatcher', scope='window',
deprecated_name='open-editor')
@cmdutils.register(instance='command-dispatcher', scope='window')
def edit_text(self):
"""Open an external editor with the currently selected form field.

View File

@ -92,6 +92,8 @@ class HintLabel(QLabel):
self._context = context
self.elem = elem
self.setTextFormat(Qt.RichText)
# Make sure we can style the background via a style sheet, and we don't
# get any extra text indent from Qt.
# The real stylesheet lives in mainwindow.py for performance reasons..
@ -1000,7 +1002,7 @@ class HintManager(QObject):
self._context.first_run = False
@cmdutils.register(instance='hintmanager', scope='window',
modes=[usertypes.KeyMode.hint], deprecated_name='follow-hint')
modes=[usertypes.KeyMode.hint])
def hint_follow(self, select: bool = False, keystring: str = None) -> None:
"""Follow a hint.

View File

@ -92,8 +92,11 @@ class CompletionMetaInfo(sql.SqlTable):
}
def __init__(self, parent=None):
super().__init__("CompletionMetaInfo", ['key', 'value'],
constraints={'key': 'PRIMARY KEY'})
self._fields = ['key', 'value']
self._constraints = {'key': 'PRIMARY KEY'}
super().__init__(
"CompletionMetaInfo", self._fields, constraints=self._constraints)
if sql.user_version_changed():
self._init_default_values()
@ -101,6 +104,15 @@ class CompletionMetaInfo(sql.SqlTable):
if key not in self.KEYS:
raise KeyError(key)
def try_recover(self):
"""Try recovering the table structure.
This should be called if getting a value via __getattr__ failed. In theory, this
should never happen, in practice, it does.
"""
self._create_table(self._fields, constraints=self._constraints, force=True)
self._init_default_values()
def _init_default_values(self):
for key, default in self.KEYS.items():
if key not in self:
@ -164,7 +176,13 @@ class WebHistory(sql.SqlTable):
self.completion = CompletionHistory(parent=self)
self.metainfo = CompletionMetaInfo(parent=self)
rebuild_completion = self.metainfo['force_rebuild']
try:
rebuild_completion = self.metainfo['force_rebuild']
except sql.BugError: # pragma: no cover
log.sql.warning("Failed to access meta info, trying to recover...",
exc_info=True)
self.metainfo.try_recover()
rebuild_completion = self.metainfo['force_rebuild']
if sql.user_version_changed():
# If the DB user version changed, run a full cleanup and rebuild the

View File

@ -29,7 +29,7 @@ from PyQt5.QtNetwork import (QNetworkProxy, QNetworkRequest, QHostInfo,
QHostAddress)
from PyQt5.QtQml import QJSEngine, QJSValue
from qutebrowser.utils import log, utils, qtutils
from qutebrowser.utils import log, utils, qtutils, resources
class ParseProxyError(Exception):
@ -190,7 +190,7 @@ class PACResolver:
self._engine.globalObject().setProperty(
"PAC", self._engine.newQObject(self._ctx))
self._evaluate(_PACContext.JS_DEFINITIONS, "pac_js_definitions")
self._evaluate(utils.read_file("javascript/pac_utils.js"), "pac_utils")
self._evaluate(resources.read_file("javascript/pac_utils.js"), "pac_utils")
proxy_config = self._engine.newObject()
proxy_config.setProperty("bindings", self._engine.newObject())
self._engine.globalObject().setProperty("ProxyConfig", proxy_config)

View File

@ -24,7 +24,7 @@ import os
from PyQt5.QtCore import QUrl, QUrlQuery
from qutebrowser.utils import utils, javascript, jinja, standarddir, log
from qutebrowser.utils import resources, javascript, jinja, standarddir, log
from qutebrowser.config import config
@ -149,7 +149,7 @@ def get_pdfjs_res_and_path(path):
if content is None:
res_path = '3rdparty/pdfjs/{}'.format(path)
try:
content = utils.read_file_binary(res_path)
content = resources.read_file_binary(res_path)
except FileNotFoundError:
raise PDFJSNotFound(path) from None
except OSError as e:

View File

@ -40,7 +40,7 @@ import qutebrowser
from qutebrowser.browser import pdfjs, downloads, history
from qutebrowser.config import config, configdata, configexc
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
objreg, standarddir)
resources, objreg, standarddir)
from qutebrowser.qt import sip
@ -271,7 +271,7 @@ def qute_javascript(url: QUrl) -> _HandlerRet:
path = url.path()
if path:
path = "javascript" + os.sep.join(path.split('/'))
return 'text/html', utils.read_file(path)
return 'text/html', resources.read_file(path)
else:
raise UrlInvalidError("No file specified")
@ -345,14 +345,14 @@ def qute_log(url: QUrl) -> _HandlerRet:
@add_handler('gpl')
def qute_gpl(_url: QUrl) -> _HandlerRet:
"""Handler for qute://gpl. Return HTML content as string."""
return 'text/html', utils.read_file('html/license.html')
return 'text/html', resources.read_file('html/license.html')
def _asciidoc_fallback_path(html_path: str) -> Optional[str]:
"""Fall back to plaintext asciidoc if the HTML is unavailable."""
path = html_path.replace('.html', '.asciidoc')
try:
return utils.read_file(path)
return resources.read_file(path)
except OSError:
return None
@ -372,14 +372,14 @@ def qute_help(url: QUrl) -> _HandlerRet:
path = 'html/doc/{}'.format(urlpath)
if not urlpath.endswith('.html'):
try:
bdata = utils.read_file_binary(path)
bdata = resources.read_file_binary(path)
except OSError as e:
raise SchemeOSError(e)
mimetype = utils.guess_mimetype(urlpath)
return mimetype, bdata
try:
data = utils.read_file(path)
data = resources.read_file(path)
except OSError:
asciidoc = _asciidoc_fallback_path(path)
@ -575,7 +575,7 @@ def qute_resource(url: QUrl) -> _HandlerRet:
path = url.path().lstrip('/')
mimetype = utils.guess_mimetype(path, fallback=True)
try:
data = utils.read_file_binary(path)
data = resources.read_file_binary(path)
except FileNotFoundError as e:
raise NotFoundError(str(e))
return mimetype, data

View File

@ -25,15 +25,15 @@ import html
import enum
import netrc
import tempfile
from typing import Callable, Mapping, List, Optional
from typing import Callable, Mapping, List, Optional, Iterable
from PyQt5.QtCore import QUrl
from PyQt5.QtCore import QUrl, pyqtBoundSignal
from qutebrowser.config import config
from qutebrowser.utils import (usertypes, message, log, objreg, jinja, utils,
qtutils)
qtutils, version)
from qutebrowser.mainwindow import mainwindow
from qutebrowser.misc import guiprocess
from qutebrowser.misc import guiprocess, objects
class CallSuper(Exception):
@ -156,54 +156,86 @@ def javascript_log_message(level, source, line, msg):
logger(logstring)
def ignore_certificate_errors(url, errors, abort_on):
def ignore_certificate_error(
*,
request_url: QUrl,
first_party_url: QUrl,
error: usertypes.AbstractCertificateErrorWrapper,
abort_on: Iterable[pyqtBoundSignal],
) -> bool:
"""Display a certificate error question.
Args:
url: The URL the errors happened in
errors: A list of QSslErrors or QWebEngineCertificateErrors
request_url: The URL of the request where the errors happened.
first_party_url: The URL of the page we're visiting. Might be an invalid QUrl.
error: A single error.
abort_on: Signals aborting a question.
Return:
True if the error should be ignored, False otherwise.
"""
ssl_strict = config.instance.get('content.ssl_strict', url=url)
log.network.debug("Certificate errors {!r}, strict {}".format(
errors, ssl_strict))
conf = config.instance.get('content.tls.certificate_errors', url=request_url)
log.network.debug(f"Certificate error {error!r}, config {conf}")
for error in errors:
assert error.is_overridable(), repr(error)
assert error.is_overridable(), repr(error)
if ssl_strict == 'ask':
# We get the first party URL with a heuristic - with HTTP -> HTTPS redirects, the
# scheme might not match.
is_resource = (
first_party_url.isValid() and
not request_url.matches(
first_party_url,
QUrl.RemoveScheme)) # type: ignore[arg-type]
if conf == 'ask' or conf == 'ask-block-thirdparty' and not is_resource:
err_template = jinja.environment.from_string("""
Errors while loading <b>{{url.toDisplayString()}}</b>:<br/>
<ul>
{% for err in errors %}
<li>{{err}}</li>
{% endfor %}
</ul>
""".strip())
msg = err_template.render(url=url, errors=errors)
{% if is_resource %}
<p>
Error while loading resource <b>{{request_url.toDisplayString()}}</b><br/>
on page <b>{{first_party_url.toDisplayString()}}</b>:
</p>
{% else %}
<p>Error while loading page <b>{{request_url.toDisplayString()}}</b>:</p>
{% endif %}
urlstr = url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
ignore = message.ask(title="Certificate errors - continue?", text=msg,
{{error.html()|safe}}
{% if is_resource %}
<p><i>Consider reporting this to the website operator, or set
<tt>content.tls.certificate_errors</tt> to <tt>ask-block-thirdparty</tt> to
always block invalid resource loads.</i></p>
{% endif %}
Do you want to ignore these errors and continue loading the page <b>insecurely</b>?
""".strip())
msg = err_template.render(
request_url=request_url,
first_party_url=first_party_url,
is_resource=is_resource,
error=error,
)
urlstr = request_url.toString(
QUrl.RemovePassword | QUrl.FullyEncoded) # type: ignore[arg-type]
ignore = message.ask(title="Certificate error", text=msg,
mode=usertypes.PromptMode.yesno, default=False,
abort_on=abort_on, url=urlstr)
if ignore is None:
# prompt aborted
ignore = False
return ignore
elif ssl_strict is False:
log.network.debug("ssl_strict is False, only warning about errors")
for err in errors:
# FIXME we might want to use warn here (non-fatal error)
# https://github.com/qutebrowser/qutebrowser/issues/114
message.error('Certificate error: {}'.format(err))
elif conf == 'load-insecurely':
message.error(f'Certificate error: {error}')
return True
elif ssl_strict is True:
elif conf == 'block':
return False
else:
raise ValueError("Invalid ssl_strict value {!r}".format(ssl_strict))
raise utils.Unreachable
elif conf == 'ask-block-thirdparty' and is_resource:
log.network.error(
f"Certificate error in resource load: {error}\n"
f" request URL: {request_url.toDisplayString()}\n"
f" first party URL: {first_party_url.toDisplayString()}")
return False
raise utils.Unreachable(conf, is_resource)
def feature_permission(url, option, msg, yes_action, no_action, abort_on,
@ -299,6 +331,15 @@ def get_user_stylesheet(searching=False):
if setting == 'never' or setting == 'when-searching' and not searching:
css += '\nhtml > ::-webkit-scrollbar { width: 0px; height: 0px; }'
if (objects.backend == usertypes.Backend.QtWebEngine and
version.qtwebengine_versions().chromium_major in [69, 73, 80, 87] and
config.val.colors.webpage.darkmode.enabled and
config.val.colors.webpage.darkmode.policy.images == 'smart' and
config.val.content.site_specific_quirks):
# WORKAROUND for MathML-output on Wikipedia being black on black.
# See https://bugs.chromium.org/p/chromium/issues/detail?id=1126606
css += '\nimg.mwe-math-fallback-image-inline { filter: invert(100%); }'
return css

View File

@ -19,6 +19,7 @@
"""Wrapper over a QWebEngineCertificateError."""
from PyQt5.QtCore import QUrl
from PyQt5.QtWebEngineWidgets import QWebEngineCertificateError
from qutebrowser.utils import usertypes, utils, debug
@ -28,21 +29,21 @@ class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
"""A wrapper over a QWebEngineCertificateError."""
def __init__(self, error):
super().__init__(error)
def __init__(self, error: QWebEngineCertificateError) -> None:
self._error = error
self.ignore = False
def __str__(self):
def __str__(self) -> str:
return self._error.errorDescription()
def __repr__(self):
def __repr__(self) -> str:
return utils.get_repr(
self, error=debug.qenum_key(QWebEngineCertificateError,
self._error.error()),
self,
error=debug.qenum_key(QWebEngineCertificateError, self._error.error()),
string=str(self))
def url(self):
def url(self) -> QUrl:
return self._error.url()
def is_overridable(self):
def is_overridable(self) -> bool:
return self._error.isOverridable()

View File

@ -342,8 +342,7 @@ def _variant(versions: version.WebEngineVersions) -> Variant:
log.init.warning(f"Ignoring invalid QUTE_DARKMODE_VARIANT={env_var}")
if (versions.webengine == utils.VersionNumber(5, 15, 2) and
versions.chromium is not None and
versions.chromium.startswith('87.')):
versions.chromium_major == 87):
# WORKAROUND for Gentoo packaging something newer as 5.15.2...
return Variant.qt_515_3
elif versions.webengine >= utils.VersionNumber(5, 15, 3):

View File

@ -28,7 +28,8 @@ from PyQt5.QtWidgets import QWidget
from qutebrowser.browser import inspector
from qutebrowser.browser.webengine import webenginesettings
from qutebrowser.misc import miscwidgets
from qutebrowser.utils import version
from qutebrowser.utils import version, usertypes
from qutebrowser.keyinput import modeman
class WebEngineInspectorView(QWebEngineView):
@ -60,9 +61,23 @@ class WebEngineInspector(inspector.AbstractWebInspector):
parent: QWidget = None) -> None:
super().__init__(splitter, win_id, parent)
self._check_devtools_resources()
view = WebEngineInspectorView()
self._settings = webenginesettings.WebEngineSettings(view.settings())
self._set_widget(view)
page = view.page()
page.windowCloseRequested.connect( # type: ignore[attr-defined]
self._on_window_close_requested)
def _on_window_close_requested(self) -> None:
"""Called when the 'x' was clicked in the devtools."""
modeman.leave(
self._win_id,
usertypes.KeyMode.insert,
'devtools close requested',
maybe=True,
)
self.hide()
def _check_devtools_resources(self) -> None:
"""Make sure that the devtools resources are available on Fedora.

View File

@ -38,7 +38,7 @@ from qutebrowser.browser.webengine import (spell, webenginequtescheme, cookies,
from qutebrowser.config import config, websettings
from qutebrowser.config.websettings import AttributeInfo as Attr
from qutebrowser.utils import (standarddir, qtutils, message, log,
urlmatch, usertypes, objreg)
urlmatch, usertypes, objreg, version)
if TYPE_CHECKING:
from qutebrowser.browser.webengine import interceptor
@ -374,7 +374,17 @@ def _init_default_profile():
default_profile = QWebEngineProfile.defaultProfile()
assert parsed_user_agent is None # avoid earlier profile initialization
non_ua_version = version.qtwebengine_versions(avoid_init=True)
init_user_agent()
ua_version = version.qtwebengine_versions()
if ua_version.webengine != non_ua_version.webengine:
log.init.warning(
"QtWebEngine version mismatch - unexpected behavior might occur, "
"please open a bug about this.\n"
f" Early version: {non_ua_version}\n"
f" Real version: {ua_version}")
default_profile.setCachePath(
os.path.join(standarddir.cache(), 'webengine'))
@ -448,6 +458,13 @@ def _init_site_specific_quirks():
pattern=urlmatch.UrlPattern(pattern),
hide_userconfig=True)
config.instance.set_obj(
'content.headers.accept_language',
'',
pattern=urlmatch.UrlPattern('https://matchmaker.krunker.io/*'),
hide_userconfig=True,
)
def _init_devtools_settings():
"""Make sure the devtools always get images/JS permissions."""
@ -489,13 +506,16 @@ def init():
from qutebrowser.misc import quitter
quitter.instance.shutting_down.connect(_download_manager.shutdown)
log.init.debug("Initializing global settings...")
global _global_settings
_global_settings = WebEngineSettings(_SettingsWrapper())
log.init.debug("Initializing profiles...")
_init_default_profile()
init_private_profile()
config.instance.changed.connect(_update_settings)
log.init.debug("Initializing site specific quirks...")
_init_site_specific_quirks()
_init_devtools_settings()

View File

@ -38,7 +38,7 @@ from qutebrowser.browser.webengine import (webview, webengineelem, tabhistory,
webengineinspector)
from qutebrowser.utils import (usertypes, qtutils, log, javascript, utils,
message, jinja, debug, version)
resources, message, jinja, debug, version)
from qutebrowser.qt import sip
from qutebrowser.misc import objects, miscwidgets
@ -1038,9 +1038,9 @@ class _WebEngineScripts(QObject):
"""Initialize global qutebrowser JavaScript."""
js_code = javascript.wrap_global(
'scripts',
utils.read_file('javascript/scroll.js'),
utils.read_file('javascript/webelem.js'),
utils.read_file('javascript/caret.js'),
resources.read_file('javascript/scroll.js'),
resources.read_file('javascript/webelem.js'),
resources.read_file('javascript/caret.js'),
)
# FIXME:qtwebengine what about subframes=True?
self._inject_js('js', js_code, subframes=True)
@ -1061,7 +1061,7 @@ class _WebEngineScripts(QObject):
css = shared.get_user_stylesheet()
js_code = javascript.wrap_global(
'stylesheet',
utils.read_file('javascript/stylesheet.js'),
resources.read_file('javascript/stylesheet.js'),
javascript.assemble('stylesheet', 'set_css', css),
)
self._inject_js('stylesheet', js_code, subframes=True)
@ -1080,18 +1080,11 @@ class _WebEngineScripts(QObject):
removed = page_scripts.remove(script)
assert removed, script.name()
def _inject_greasemonkey_scripts(self, scripts=None, injection_point=None,
remove_first=True):
def _inject_greasemonkey_scripts(self, scripts):
"""Register user JavaScript files with the current tab.
Args:
scripts: A list of GreasemonkeyScripts, or None to add all
known by the Greasemonkey subsystem.
injection_point: The QWebEngineScript::InjectionPoint stage
to inject the script into, None to use
auto-detection.
remove_first: Whether to remove all previously injected
scripts before adding these ones.
scripts: A list of GreasemonkeyScripts.
"""
if sip.isdeleted(self._widget):
return
@ -1102,49 +1095,49 @@ class _WebEngineScripts(QObject):
# While, taking care not to remove any other scripts that might
# have been added elsewhere, like the one for stylesheets.
page_scripts = self._widget.page().scripts()
if remove_first:
self._remove_all_greasemonkey_scripts()
if not scripts:
return
self._remove_all_greasemonkey_scripts()
for script in scripts:
new_script = QWebEngineScript()
try:
world = int(script.jsworld)
if not 0 <= world <= qtutils.MAX_WORLD_ID:
log.greasemonkey.error(
"script {} has invalid value for '@qute-js-world'"
": {}, should be between 0 and {}"
.format(
script.name,
script.jsworld,
qtutils.MAX_WORLD_ID))
f"script {script.name} has invalid value for '@qute-js-world'"
f": {script.jsworld}, should be between 0 and "
f"{qtutils.MAX_WORLD_ID}")
continue
except ValueError:
try:
world = _JS_WORLD_MAP[usertypes.JsWorld[
script.jsworld.lower()]]
world = _JS_WORLD_MAP[usertypes.JsWorld[script.jsworld.lower()]]
except KeyError:
log.greasemonkey.error(
"script {} has invalid value for '@qute-js-world'"
": {}".format(script.name, script.jsworld))
f"script {script.name} has invalid value for '@qute-js-world'"
f": {script.jsworld}")
continue
new_script.setWorldId(world)
# Corresponds to "@run-at document-end" which is the default according to
# https://wiki.greasespot.net/Metadata_Block#.40run-at - however,
# QtWebEngine uses QWebEngineScript.Deferred (@run-at document-idle) as
# default.
#
# NOTE that this needs to be done before setSourceCode, so that
# QtWebEngine's parsing of GreaseMonkey tags will override it if there is a
# @run-at comment.
new_script.setInjectionPoint(QWebEngineScript.DocumentReady)
new_script.setSourceCode(script.code())
new_script.setName("GM-{}".format(script.name))
new_script.setName(f"GM-{script.name}")
new_script.setRunsOnSubFrames(script.runs_on_sub_frames)
# Override the @run-at value parsed by QWebEngineScript if desired.
if injection_point:
new_script.setInjectionPoint(injection_point)
elif script.needs_document_end_workaround():
log.greasemonkey.debug("Forcing @run-at document-end for {}"
.format(script.name))
if script.needs_document_end_workaround():
log.greasemonkey.debug(
f"Forcing @run-at document-end for {script.name}")
new_script.setInjectionPoint(QWebEngineScript.DocumentReady)
log.greasemonkey.debug('adding script: {}'
.format(new_script.name()))
log.greasemonkey.debug(f'adding script: {new_script.name()}')
page_scripts.insert(new_script)
def _inject_site_specific_quirks(self):
@ -1176,7 +1169,7 @@ class _WebEngineScripts(QObject):
for quirk in quirks:
if not quirk.predicate:
continue
src = utils.read_file(f'javascript/quirks/{quirk.filename}.user.js')
src = resources.read_file(f'javascript/quirks/{quirk.filename}.user.js')
self._inject_js(
f'quirk_{quirk.filename}',
src,
@ -1523,11 +1516,22 @@ class WebEngineTab(browsertab.AbstractTab):
url = error.url()
self._insecure_hosts.add(url.host())
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-92009
# self.url() is not available yet and the requested URL might not match the URL
# we get from the error - so we just apply a heuristic here.
assert self.data.last_navigation is not None
first_party_url = self.data.last_navigation.url
log.network.debug("Certificate error: {}".format(error))
log.network.debug("First party URL: {}".format(first_party_url))
if error.is_overridable():
error.ignore = shared.ignore_certificate_errors(
url, [error], abort_on=[self.abort_questions])
error.ignore = shared.ignore_certificate_error(
request_url=url,
first_party_url=first_party_url,
error=error,
abort_on=[self.abort_questions],
)
else:
log.network.error("Non-overridable certificate error: "
"{}".format(error))
@ -1551,12 +1555,10 @@ class WebEngineTab(browsertab.AbstractTab):
# We can't really know when to show an error page, as the error might
# have happened when loading some resource.
# However, self.url() is not available yet and the requested URL
# might not match the URL we get from the error - so we just apply a
# heuristic here.
assert self.data.last_navigation is not None
if (show_non_overr_cert_error and
url.matches(self.data.last_navigation.url, QUrl.RemoveScheme)):
is_resource = (
first_party_url.isValid() and
url.matches(first_party_url, QUrl.RemoveScheme))
if show_non_overr_cert_error and is_resource:
self._show_error_page(url, str(error))
@pyqtSlot()

View File

@ -17,31 +17,51 @@
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
"""Wrapper over a QSslError."""
"""A wrapper over a list of QSslErrors."""
from typing import Sequence
from PyQt5.QtNetwork import QSslError
from qutebrowser.utils import usertypes, utils, debug
from qutebrowser.utils import usertypes, utils, debug, jinja
class CertificateErrorWrapper(usertypes.AbstractCertificateErrorWrapper):
"""A wrapper over a QSslError."""
"""A wrapper over a list of QSslErrors."""
def __str__(self):
return self._error.errorString()
def __init__(self, errors: Sequence[QSslError]) -> None:
self._errors = tuple(errors) # needs to be hashable
def __repr__(self):
def __str__(self) -> str:
return '\n'.join(err.errorString() for err in self._errors)
def __repr__(self) -> str:
return utils.get_repr(
self, error=debug.qenum_key(QSslError, self._error.error()),
self,
errors=[debug.qenum_key(QSslError, err.error()) for err in self._errors],
string=str(self))
def __hash__(self):
return hash(self._error)
def __hash__(self) -> int:
return hash(self._errors)
def __eq__(self, other):
return self._error == other._error
def __eq__(self, other: object) -> bool:
if not isinstance(other, CertificateErrorWrapper):
return NotImplemented
return self._errors == other._errors
def is_overridable(self):
def is_overridable(self) -> bool:
return True
def html(self):
if len(self._errors) == 1:
return super().html()
template = jinja.environment.from_string("""
<ul>
{% for err in errors %}
<li>{{err.errorString()}}</li>
{% endfor %}
</ul>
""".strip())
return template.render(errors=self._errors)

View File

@ -22,11 +22,11 @@
import collections
import html
import dataclasses
from typing import TYPE_CHECKING, Dict, MutableMapping, Optional, Sequence
from typing import TYPE_CHECKING, Dict, MutableMapping, Optional, Set
from PyQt5.QtCore import pyqtSlot, pyqtSignal, QUrl, QByteArray
from PyQt5.QtNetwork import (QNetworkAccessManager, QNetworkReply, QSslSocket,
QSslError, QNetworkProxy)
QNetworkProxy)
from qutebrowser.config import config
from qutebrowser.utils import (message, log, usertypes, utils, objreg,
@ -122,7 +122,10 @@ def init():
QSslSocket.setDefaultCiphers(good_ciphers)
_SavedErrorsType = MutableMapping[urlutils.HostTupleType, Sequence[QSslError]]
_SavedErrorsType = MutableMapping[
urlutils.HostTupleType,
Set[certificateerror.CertificateErrorWrapper],
]
class NetworkManager(QNetworkAccessManager):
@ -141,8 +144,8 @@ class NetworkManager(QNetworkAccessManager):
(or None for generic network managers)
_tab_id: The tab ID this NetworkManager is associated with.
(or None for generic network managers)
_rejected_ssl_errors: A {QUrl: [SslError]} dict of rejected errors.
_accepted_ssl_errors: A {QUrl: [SslError]} dict of accepted errors.
_rejected_ssl_errors: A {QUrl: {SslError}} dict of rejected errors.
_accepted_ssl_errors: A {QUrl: {SslError}} dict of accepted errors.
_private: Whether we're in private browsing mode.
netrc_used: Whether netrc authentication was performed.
@ -172,8 +175,8 @@ class NetworkManager(QNetworkAccessManager):
self._set_cookiejar()
self._set_cache()
self.sslErrors.connect(self.on_ssl_errors)
self._rejected_ssl_errors: _SavedErrorsType = collections.defaultdict(list)
self._accepted_ssl_errors: _SavedErrorsType = collections.defaultdict(list)
self._rejected_ssl_errors: _SavedErrorsType = collections.defaultdict(set)
self._accepted_ssl_errors: _SavedErrorsType = collections.defaultdict(set)
self.authenticationRequired.connect(self.on_authentication_required)
self.proxyAuthenticationRequired.connect(self.on_proxy_authentication_required)
self.netrc_used = False
@ -214,6 +217,25 @@ class NetworkManager(QNetworkAccessManager):
abort_on.append(tab.load_started)
return abort_on
def _get_tab(self):
"""Get the tab this NAM is associated with.
Return:
The tab if available, None otherwise.
"""
# There are some scenarios where we can't figure out current_url:
# - There's a generic NetworkManager, e.g. for downloads
# - The download was in a tab which is now closed.
if self._tab_id is None:
return None
assert self._win_id is not None
try:
return objreg.get('tab', scope='tab', window=self._win_id, tab=self._tab_id)
except KeyError:
# https://github.com/qutebrowser/qutebrowser/issues/889
return None
def shutdown(self):
"""Abort all running requests."""
self.setNetworkAccessible(QNetworkAccessManager.NotAccessible)
@ -221,18 +243,17 @@ class NetworkManager(QNetworkAccessManager):
# No @pyqtSlot here, see
# https://github.com/qutebrowser/qutebrowser/issues/2213
def on_ssl_errors(self, reply, errors): # noqa: C901 pragma: no mccabe
def on_ssl_errors(self, reply, qt_errors): # noqa: C901 pragma: no mccabe
"""Decide if SSL errors should be ignored or not.
This slot is called on SSL/TLS errors by the self.sslErrors signal.
Args:
reply: The QNetworkReply that is encountering the errors.
errors: A list of errors.
qt_errors: A list of errors.
"""
errors = [certificateerror.CertificateErrorWrapper(e) for e in errors]
log.network.debug("Certificate errors: {!r}".format(
' / '.join(str(err) for err in errors)))
errors = certificateerror.CertificateErrorWrapper(qt_errors)
log.network.debug("Certificate errors: {!r}".format(errors))
try:
host_tpl: Optional[urlutils.HostTupleType] = urlutils.host_tuple(
reply.url())
@ -242,10 +263,8 @@ class NetworkManager(QNetworkAccessManager):
is_rejected = False
else:
assert host_tpl is not None
is_accepted = set(errors).issubset(
self._accepted_ssl_errors[host_tpl])
is_rejected = set(errors).issubset(
self._rejected_ssl_errors[host_tpl])
is_accepted = errors in self._accepted_ssl_errors[host_tpl]
is_rejected = errors in self._rejected_ssl_errors[host_tpl]
log.network.debug("Already accepted: {} / "
"rejected {}".format(is_accepted, is_rejected))
@ -257,15 +276,22 @@ class NetworkManager(QNetworkAccessManager):
return
abort_on = self._get_abort_signals(reply)
ignore = shared.ignore_certificate_errors(reply.url(), errors,
abort_on=abort_on)
tab = self._get_tab()
first_party_url = QUrl() if tab is None else tab.data.last_navigation.url
ignore = shared.ignore_certificate_error(
request_url=reply.url(),
first_party_url=first_party_url,
error=errors,
abort_on=abort_on,
)
if ignore:
reply.ignoreSslErrors()
err_dict = self._accepted_ssl_errors
else:
err_dict = self._rejected_ssl_errors
if host_tpl is not None:
err_dict[host_tpl] += errors
if host_tpl is not None:
self._accepted_ssl_errors[host_tpl].add(errors)
elif host_tpl is not None:
self._rejected_ssl_errors[host_tpl].add(errors)
def clear_all_ssl_errors(self):
"""Clear all remembered SSL errors."""
@ -392,22 +418,14 @@ class NetworkManager(QNetworkAccessManager):
for header, value in shared.custom_headers(url=req.url()):
req.setRawHeader(header, value)
# There are some scenarios where we can't figure out current_url:
# - There's a generic NetworkManager, e.g. for downloads
# - The download was in a tab which is now closed.
tab = self._get_tab()
current_url = QUrl()
if self._tab_id is not None:
assert self._win_id is not None
if tab is not None:
try:
tab = objreg.get('tab', scope='tab', window=self._win_id,
tab=self._tab_id)
current_url = tab.url()
except (KeyError, RuntimeError):
# https://github.com/qutebrowser/qutebrowser/issues/889
# Catching RuntimeError because we could be in the middle of
# the webpage shutdown here.
current_url = QUrl()
except RuntimeError:
# We could be in the middle of the webpage shutdown here.
pass
request = interceptors.Request(first_party_url=current_url,
request_url=req.url())

View File

@ -34,8 +34,7 @@ from PyQt5.QtPrintSupport import QPrinter
from qutebrowser.browser import browsertab, shared
from qutebrowser.browser.webkit import (webview, tabhistory, webkitelem,
webkitsettings, webkitinspector)
from qutebrowser.utils import qtutils, usertypes, utils, log, debug
from qutebrowser.utils import qtutils, usertypes, utils, log, debug, resources
from qutebrowser.keyinput import modeman
from qutebrowser.qt import sip
@ -228,7 +227,7 @@ class WebKitCaret(browsertab.AbstractCaret):
# true in caret mode.
if self._selection_state is browsertab.SelectionState.none:
self._widget.page().currentFrame().evaluateJavaScript(
utils.read_file('javascript/position_caret.js'))
resources.read_file('javascript/position_caret.js'))
@pyqtSlot(usertypes.KeyMode)
def _on_mode_left(self, _mode):

View File

@ -0,0 +1,209 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2021 Ryan Roden-Corrent (rcorre) <ryan@rcorre.net>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
"""Module for parsing commands entered into the browser."""
import dataclasses
from typing import List, Iterator
from qutebrowser.commands import cmdexc, command
from qutebrowser.misc import split, objects
from qutebrowser.config import config
@dataclasses.dataclass
class ParseResult:
"""The result of parsing a commandline."""
cmd: command.Command
args: List[str]
cmdline: List[str]
class CommandParser:
"""Parse qutebrowser commandline commands.
Attributes:
_partial_match: Whether to allow partial command matches.
"""
def __init__(self, partial_match: bool = False) -> None:
self._partial_match = partial_match
def _get_alias(self, text: str, *, default: str) -> str:
"""Get an alias from the config.
Args:
text: The text to parse.
aliases: A map of aliases to commands.
default : Default value to return when alias was not found.
Return:
The new command string if an alias was found. Default value
otherwise.
"""
parts = text.strip().split(maxsplit=1)
aliases = config.cache['aliases']
if parts[0] not in aliases:
return default
alias = aliases[parts[0]]
try:
new_cmd = '{} {}'.format(alias, parts[1])
except IndexError:
new_cmd = alias
if text.endswith(' '):
new_cmd += ' '
return new_cmd
def _parse_all_gen(
self,
text: str,
aliases: bool = True,
**kwargs: bool,
) -> Iterator[ParseResult]:
"""Split a command on ;; and parse all parts.
If the first command in the commandline is a non-split one, it only
returns that.
Args:
text: Text to parse.
aliases: Whether to handle aliases.
**kwargs: Passed to parse().
Yields:
ParseResult tuples.
"""
text = text.strip().lstrip(':').strip()
if not text:
raise cmdexc.NoSuchCommandError("No command given")
if aliases:
text = self._get_alias(text, default=text)
if ';;' in text:
# Get the first command and check if it doesn't want to have ;;
# split.
first = text.split(';;')[0]
result = self.parse(first, **kwargs)
if result.cmd.no_cmd_split:
sub_texts = [text]
else:
sub_texts = [e.strip() for e in text.split(';;')]
else:
sub_texts = [text]
for sub in sub_texts:
yield self.parse(sub, **kwargs)
def parse_all(self, text: str, **kwargs: bool) -> List[ParseResult]:
"""Wrapper over _parse_all_gen."""
return list(self._parse_all_gen(text, **kwargs))
def parse(self, text: str, *, keep: bool = False) -> ParseResult:
"""Split the commandline text into command and arguments.
Args:
text: Text to parse.
keep: Whether to keep special chars and whitespace.
"""
cmdstr, sep, argstr = text.partition(' ')
if not cmdstr:
raise cmdexc.NoSuchCommandError("No command given")
if self._partial_match:
cmdstr = self._completion_match(cmdstr)
try:
cmd = objects.commands[cmdstr]
except KeyError:
raise cmdexc.NoSuchCommandError(f'{cmdstr}: no such command')
args = self._split_args(cmd, argstr, keep)
if keep and args:
cmdline = [cmdstr, sep + args[0]] + args[1:]
elif keep:
cmdline = [cmdstr, sep]
else:
cmdline = [cmdstr] + args[:]
return ParseResult(cmd=cmd, args=args, cmdline=cmdline)
def _completion_match(self, cmdstr: str) -> str:
"""Replace cmdstr with a matching completion if there's only one match.
Args:
cmdstr: The string representing the entered command so far.
Return:
cmdstr modified to the matching completion or unmodified
"""
matches = [cmd for cmd in sorted(objects.commands, key=len)
if cmdstr in cmd]
if len(matches) == 1:
cmdstr = matches[0]
elif len(matches) > 1 and config.val.completion.use_best_match:
cmdstr = matches[0]
return cmdstr
def _split_args(self, cmd: command.Command, argstr: str, keep: bool) -> List[str]:
"""Split the arguments from an arg string.
Args:
cmd: The command we're currently handling.
argstr: An argument string.
keep: Whether to keep special chars and whitespace
Return:
A list containing the split strings.
"""
if not argstr:
return []
elif cmd.maxsplit is None:
return split.split(argstr, keep=keep)
else:
# If split=False, we still want to split the flags, but not
# everything after that.
# We first split the arg string and check the index of the first
# non-flag args, then we re-split again properly.
# example:
#
# input: "--foo -v bar baz"
# first split: ['--foo', '-v', 'bar', 'baz']
# 0 1 2 3
# second split: ['--foo', '-v', 'bar baz']
# (maxsplit=2)
split_args = split.simple_split(argstr, keep=keep)
flag_arg_count = 0
for i, arg in enumerate(split_args):
arg = arg.strip()
if arg.startswith('-'):
if arg in cmd.flags_with_args:
flag_arg_count += 1
else:
maxsplit = i + cmd.maxsplit + flag_arg_count
return split.simple_split(argstr, keep=keep,
maxsplit=maxsplit)
# If there are only flags, we got it right on the first try
# already.
return split_args

View File

@ -22,17 +22,13 @@
import traceback
import re
import contextlib
import dataclasses
from typing import (TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping,
List, Optional)
from typing import TYPE_CHECKING, Callable, Dict, Iterator, Mapping, MutableMapping
from PyQt5.QtCore import pyqtSlot, QUrl, QObject
from qutebrowser.api import cmdutils
from qutebrowser.config import config
from qutebrowser.commands import cmdexc, command
from qutebrowser.commands import cmdexc, parser
from qutebrowser.utils import message, objreg, qtutils, usertypes, utils
from qutebrowser.misc import split, objects
from qutebrowser.keyinput import macros, modeman
if TYPE_CHECKING:
@ -43,16 +39,6 @@ _ReplacementFunction = Callable[['tabbedbrowser.TabbedBrowser'], str]
last_command = {}
@dataclasses.dataclass
class ParseResult:
"""The result of parsing a commandline."""
cmd: Optional[command.Command]
args: Optional[List[str]]
cmdline: List[str]
def _url(tabbed_browser):
"""Convenience method to get the current url."""
try:
@ -130,181 +116,6 @@ def replace_variables(win_id, arglist):
return args
class CommandParser:
"""Parse qutebrowser commandline commands.
Attributes:
_partial_match: Whether to allow partial command matches.
"""
def __init__(self, partial_match=False):
self._partial_match = partial_match
def _get_alias(self, text, default=None):
"""Get an alias from the config.
Args:
text: The text to parse.
default : Default value to return when alias was not found.
Return:
The new command string if an alias was found. Default value
otherwise.
"""
parts = text.strip().split(maxsplit=1)
aliases = config.cache['aliases']
if parts[0] not in aliases:
return default
alias = aliases[parts[0]]
try:
new_cmd = '{} {}'.format(alias, parts[1])
except IndexError:
new_cmd = alias
if text.endswith(' '):
new_cmd += ' '
return new_cmd
def _parse_all_gen(self, text, *args, aliases=True, **kwargs):
"""Split a command on ;; and parse all parts.
If the first command in the commandline is a non-split one, it only
returns that.
Args:
text: Text to parse.
aliases: Whether to handle aliases.
*args/**kwargs: Passed to parse().
Yields:
ParseResult tuples.
"""
text = text.strip().lstrip(':').strip()
if not text:
raise cmdexc.NoSuchCommandError("No command given")
if aliases:
text = self._get_alias(text, text)
if ';;' in text:
# Get the first command and check if it doesn't want to have ;;
# split.
first = text.split(';;')[0]
result = self.parse(first, *args, **kwargs)
if result.cmd.no_cmd_split:
sub_texts = [text]
else:
sub_texts = [e.strip() for e in text.split(';;')]
else:
sub_texts = [text]
for sub in sub_texts:
yield self.parse(sub, *args, **kwargs)
def parse_all(self, *args, **kwargs):
"""Wrapper over _parse_all_gen."""
return list(self._parse_all_gen(*args, **kwargs))
def parse(self, text, *, fallback=False, keep=False):
"""Split the commandline text into command and arguments.
Args:
text: Text to parse.
fallback: Whether to do a fallback splitting when the command was
unknown.
keep: Whether to keep special chars and whitespace
Return:
A ParseResult tuple.
"""
cmdstr, sep, argstr = text.partition(' ')
if not cmdstr and not fallback:
raise cmdexc.NoSuchCommandError("No command given")
if self._partial_match:
cmdstr = self._completion_match(cmdstr)
try:
cmd = objects.commands[cmdstr]
except KeyError:
if not fallback:
raise cmdexc.NoSuchCommandError(
'{}: no such command'.format(cmdstr))
cmdline = split.split(text, keep=keep)
return ParseResult(cmd=None, args=None, cmdline=cmdline)
args = self._split_args(cmd, argstr, keep)
if keep and args:
cmdline = [cmdstr, sep + args[0]] + args[1:]
elif keep:
cmdline = [cmdstr, sep]
else:
cmdline = [cmdstr] + args[:]
return ParseResult(cmd=cmd, args=args, cmdline=cmdline)
def _completion_match(self, cmdstr):
"""Replace cmdstr with a matching completion if there's only one match.
Args:
cmdstr: The string representing the entered command so far
Return:
cmdstr modified to the matching completion or unmodified
"""
matches = [cmd for cmd in sorted(objects.commands, key=len)
if cmdstr in cmd]
if len(matches) == 1:
cmdstr = matches[0]
elif len(matches) > 1 and config.val.completion.use_best_match:
cmdstr = matches[0]
return cmdstr
def _split_args(self, cmd, argstr, keep):
"""Split the arguments from an arg string.
Args:
cmd: The command we're currently handling.
argstr: An argument string.
keep: Whether to keep special chars and whitespace
Return:
A list containing the split strings.
"""
if not argstr:
return []
elif cmd.maxsplit is None:
return split.split(argstr, keep=keep)
else:
# If split=False, we still want to split the flags, but not
# everything after that.
# We first split the arg string and check the index of the first
# non-flag args, then we re-split again properly.
# example:
#
# input: "--foo -v bar baz"
# first split: ['--foo', '-v', 'bar', 'baz']
# 0 1 2 3
# second split: ['--foo', '-v', 'bar baz']
# (maxsplit=2)
split_args = split.simple_split(argstr, keep=keep)
flag_arg_count = 0
for i, arg in enumerate(split_args):
arg = arg.strip()
if arg.startswith('-'):
if arg in cmd.flags_with_args:
flag_arg_count += 1
else:
maxsplit = i + cmd.maxsplit + flag_arg_count
return split.simple_split(argstr, keep=keep,
maxsplit=maxsplit)
# If there are only flags, we got it right on the first try
# already.
return split_args
class AbstractCommandRunner(QObject):
"""Abstract base class for CommandRunner."""
@ -329,7 +140,7 @@ class CommandRunner(AbstractCommandRunner):
def __init__(self, win_id, partial_match=False, parent=None):
super().__init__(parent)
self._parser = CommandParser(partial_match=partial_match)
self._parser = parser.CommandParser(partial_match=partial_match)
self._win_id = win_id
@contextlib.contextmanager
@ -362,7 +173,7 @@ class CommandRunner(AbstractCommandRunner):
parsed = self._parser.parse_all(text)
if parsed is None:
return
return # type: ignore[unreachable]
for result in parsed:
with self._handle_error(safely):

View File

@ -25,8 +25,8 @@ from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, QObject, QTimer
from qutebrowser.config import config
from qutebrowser.commands import runners
from qutebrowser.misc import objects
from qutebrowser.commands import parser, cmdexc
from qutebrowser.misc import objects, split
from qutebrowser.utils import log, utils, debug, objreg
from qutebrowser.completion.models import miscmodels
if TYPE_CHECKING:
@ -139,13 +139,18 @@ class Completer(QObject):
if not text or not text.strip():
# Only ":", empty part under the cursor with nothing before/after
return [], '', []
parser = runners.CommandParser()
result = parser.parse(text, fallback=True, keep=True)
parts = [x for x in result.cmdline if x]
try:
parse_result = parser.CommandParser().parse(text, keep=True)
except cmdexc.NoSuchCommandError:
cmdline = split.split(text, keep=True)
else:
cmdline = parse_result.cmdline
parts = [x for x in cmdline if x]
pos = self._cmd.cursorPosition() - len(self._cmd.prefix())
pos = min(pos, len(text)) # Qt treats 2-byte UTF-16 chars as 2 chars
log.completion.debug('partitioning {} around position {}'.format(parts,
pos))
log.completion.debug(f'partitioning {parts} around position {pos}')
for i, part in enumerate(parts):
pos -= len(part)
if pos <= 0:
@ -156,11 +161,10 @@ class Completer(QObject):
center = parts[i].strip()
# strip trailing whitespace included as a separate token
postfix = [x.strip() for x in parts[i+1:] if not x.isspace()]
log.completion.debug(
"partitioned: {} '{}' {}".format(prefix, center, postfix))
log.completion.debug(f"partitioned: {prefix} '{center}' {postfix}")
return prefix, center, postfix
raise utils.Unreachable("Not all parts consumed: {}".format(parts))
raise utils.Unreachable(f"Not all parts consumed: {parts}")
@pyqtSlot(str)
def on_selection_changed(self, text):

View File

@ -21,7 +21,7 @@
from qutebrowser.config import configdata, configexc
from qutebrowser.completion.models import completionmodel, listcategory, util
from qutebrowser.commands import runners, cmdexc
from qutebrowser.commands import parser, cmdexc
from qutebrowser.keyinput import keyutils
@ -117,9 +117,8 @@ def _bind_current_default(key, info):
cmd_text = info.keyconf.get_command(seq, 'normal')
if cmd_text:
parser = runners.CommandParser()
try:
cmd = parser.parse(cmd_text).cmd
cmd = parser.CommandParser().parse(cmd_text).cmd
except cmdexc.NoSuchCommandError:
data.append((cmd_text, '(Current) Invalid command!', key))
else:
@ -127,8 +126,7 @@ def _bind_current_default(key, info):
cmd_text = info.keyconf.get_command(seq, 'normal', default=True)
if cmd_text:
parser = runners.CommandParser()
cmd = parser.parse(cmd_text).cmd
cmd = parser.CommandParser().parse(cmd_text).cmd
data.append((cmd_text, '(Default) {}'.format(cmd.desc), key))
return data

View File

@ -203,7 +203,13 @@ class BraveAdBlocker:
def read_cache(self) -> None:
"""Initialize the adblocking engine from cache file."""
if self._cache_path.is_file():
try:
cache_exists = self._cache_path.is_file()
except OSError:
logger.error("Failed to read adblock cache", exc_info=True)
return
if cache_exists:
logger.debug("Loading cached adblock data: %s", self._cache_path)
self._engine.deserialize_from_file(str(self._cache_path))
else:

View File

@ -183,7 +183,7 @@ def move_to_end_of_document(tab: apitypes.Tab) -> None:
tab.caret.move_to_end_of_document()
@cmdutils.register(modes=[cmdutils.KeyMode.caret], deprecated_name='toggle-selection')
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def selection_toggle(tab: apitypes.Tab, line: bool = False) -> None:
"""Toggle caret selection mode.
@ -194,14 +194,14 @@ def selection_toggle(tab: apitypes.Tab, line: bool = False) -> None:
tab.caret.toggle_selection(line)
@cmdutils.register(modes=[cmdutils.KeyMode.caret], deprecated_name='drop-selection')
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def selection_drop(tab: apitypes.Tab) -> None:
"""Drop selection and keep selection mode enabled."""
tab.caret.drop_selection()
@cmdutils.register(deprecated_name='follow-selected')
@cmdutils.register()
@cmdutils.argument('tab_obj', value=cmdutils.Value.cur_tab)
def selection_follow(tab_obj: apitypes.Tab, *, tab: bool = False) -> None:
"""Follow the selected text.
@ -215,7 +215,7 @@ def selection_follow(tab_obj: apitypes.Tab, *, tab: bool = False) -> None:
raise cmdutils.CommandError(str(e))
@cmdutils.register(modes=[cmdutils.KeyMode.caret], deprecated_name='reverse-selection')
@cmdutils.register(modes=[cmdutils.KeyMode.caret])
@cmdutils.argument('tab', value=cmdutils.Value.cur_tab)
def selection_reverse(tab: apitypes.Tab) -> None:
"""Swap the stationary and moving end of the current selection."""

View File

@ -27,6 +27,7 @@ from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Mapping,
from PyQt5.QtCore import pyqtSignal, QObject, QUrl
from qutebrowser.commands import cmdexc, parser
from qutebrowser.config import configdata, configexc, configutils
from qutebrowser.utils import utils, log, urlmatch
from qutebrowser.misc import objects
@ -162,13 +163,38 @@ class KeyConfig:
bindings[key] = binding
return bindings
def _implied_cmd(self, cmdline: str) -> Optional[str]:
"""Return cmdline, or the implied cmd if cmdline is a set-cmd-text."""
try:
results = parser.CommandParser().parse_all(cmdline)
except cmdexc.NoSuchCommandError:
return None
result = results[0]
if result.cmd.name != "set-cmd-text":
return cmdline
*flags, cmd = result.args
if "-a" in flags or "--append" in flags or not cmd.startswith(":"):
return None # doesn't look like this sets a command
return cmd.lstrip(":")
def get_reverse_bindings_for(self, mode: str) -> '_ReverseBindings':
"""Get a dict of commands to a list of bindings for the mode."""
"""Get a dict of commands to a list of bindings for the mode.
This is intented for user-facing display of keybindings.
As such, bindings for 'set-cmd-text [flags] :<cmd> ...' are translated
to '<cmd> ...', as from the user's perspective these keys behave like
bindings for '<cmd>' (that allow for further input before running).
See #5942.
"""
cmd_to_keys: KeyConfig._ReverseBindings = {}
bindings = self.get_bindings_for(mode)
for seq, full_cmd in sorted(bindings.items()):
for cmd in full_cmd.split(';;'):
cmd = cmd.strip()
for cmdtext in full_cmd.split(';;'):
cmd = self._implied_cmd(cmdtext.strip())
if not cmd:
continue
cmd_to_keys.setdefault(cmd, [])
# Put bindings involving modifiers last
if any(info.modifiers for info in seq):

View File

@ -30,7 +30,7 @@ import functools
import dataclasses
from qutebrowser.config import configtypes
from qutebrowser.utils import usertypes, qtutils, utils
from qutebrowser.utils import usertypes, qtutils, utils, resources
from qutebrowser.misc import debugcachestats
DATA = cast(Mapping[str, 'Option'], None)
@ -272,4 +272,4 @@ def is_valid_prefix(prefix: str) -> bool:
def init() -> None:
"""Initialize configdata from the YAML file."""
global DATA, MIGRATIONS
DATA, MIGRATIONS = _read_yaml(utils.read_file('config/configdata.yml'))
DATA, MIGRATIONS = _read_yaml(resources.read_file('config/configdata.yml'))

View File

@ -311,6 +311,20 @@ qt.workarounds.remove_service_workers:
Note however that enabling this option *can lead to data loss* on some pages (as
Service Worker data isn't persisted) and will negatively impact start-up time.
qt.workarounds.locale:
type: Bool
default: false
backend: QtWebEngine
desc: >-
Work around locale parsing issues in QtWebEngine 5.15.3.
With some locales, QtWebEngine 5.15.3 is unusable without this workaround.
In affected scenarios, QtWebEngine will log "Network service crashed,
restarting service." and only display a blank page.
However, It is expected that distributions shipping QtWebEngine 5.15.3
follow up with a proper fix soon, so it is disabled by default.
## auto_save
auto_save.interval:
@ -607,14 +621,14 @@ content.headers.user_agent:
# Vim-protip: Place your cursor below this comment and run
# :r!python scripts/dev/ua_fetch.py
- - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like
Gecko) Chrome/87.0.4280.66 Safari/537.36"
- Chrome 87 Linux
Gecko) Chrome/88.0.4324.96 Safari/537.36"
- Chrome 88 Linux
- - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,
like Gecko) Chrome/87.0.4280.66 Safari/537.36"
- Chrome 87 Win10
like Gecko) Chrome/88.0.4324.104 Safari/537.36"
- Chrome 88 Win10
- - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36
(KHTML, like Gecko) Chrome/87.0.4280.67 Safari/537.36"
- Chrome 87 macOS
(KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36"
- Chrome 88 macOS
supports_pattern: true
desc: |
User agent to send.
@ -939,11 +953,22 @@ content.register_protocol_handler:
desc: Allow websites to register protocol handlers via
`navigator.registerProtocolHandler`.
content.ssl_strict:
content.tls.certificate_errors:
default: ask
type: BoolAsk
type:
name: String
valid_values:
- ask: Ask how to proceed for every certificate error (unless non-overridable due
to HSTS).
- ask-block-thirdparty: Ask how to proceed for normal page loads, but silently
block resource loads.
- block: Automatically block loading on certificate errors.
- load-insecurely: Force loading pages despite certificate errors. This is
*insecure* and should be avoided. Instead of using this, consider fixing the
underlying issue or importing a self-signed certificate via `certutil` (or
Chromium) instead.
supports_pattern: true
desc: Validate SSL handshakes.
desc: How to proceed on TLS certificate errors.
content.user_stylesheets:
type:
@ -1617,6 +1642,18 @@ input.spatial_navigation:
Right key, heuristics determine whether there is an element he might be
trying to reach towards the right and which element he probably wants.
input.media_keys:
default: true
type: Bool
backend:
QtWebEngine: Qt 5.14
QtWebKit: false
restart: true
desc: >-
Whether the underlying Chromium should handle media keys.
On Linux, disabling this also disables Chromium's MPRIS integration.
## keyhint
keyhint.blacklist:
@ -3236,13 +3273,20 @@ bindings.key_mappings:
keytype: Key
valtype: Key
desc: >-
This setting can be used to map keys to other keys.
Map keys to other keys, so that they are equivalent in all modes.
When the key used as dictionary-key is pressed, the binding for the key
used as dictionary-value is invoked instead.
This is useful for global remappings of keys, for example to map Ctrl-[ to
Escape.
This is useful for global remappings of keys, for example to map <Ctrl-[> to
<Escape>.
NOTE: This should only be used if two keys should always be equivalent, i.e. for
things like <Enter> (keypad) and <Return> (non-keypad). For normal command bindings,
qutebrowser works differently to vim: You always bind keys to commands, usually via
`:bind` or `config.bind()`. Instead of using this setting, consider finding the
command a key is bound to (e.g. via `:bind gg`) and then binding the same command to
the desired key.
Note that when a key is bound (via `bindings.default` or
`bindings.commands`), the mapping is ignored.

View File

@ -128,22 +128,21 @@ class StateConfig(configparser.ConfigParser):
# https://github.com/python/typeshed/issues/2093
return # type: ignore[unreachable]
old_version = utils.parse_version(old_qutebrowser_version)
new_version = utils.parse_version(qutebrowser.__version__)
if old_version.isNull():
try:
old_version = utils.VersionNumber.parse(old_qutebrowser_version)
except ValueError:
log.init.warning(f"Unable to parse old version {old_qutebrowser_version}")
return
assert not new_version.isNull(), qutebrowser.__version__
new_version = utils.VersionNumber.parse(qutebrowser.__version__)
if old_version == new_version:
self.qutebrowser_version_changed = VersionChange.equal
elif new_version < old_version:
self.qutebrowser_version_changed = VersionChange.downgrade
elif old_version.segments()[:2] == new_version.segments()[:2]:
elif old_version.segments[:2] == new_version.segments[:2]:
self.qutebrowser_version_changed = VersionChange.patch
elif old_version.majorVersion() == new_version.majorVersion():
elif old_version.major == new_version.major:
self.qutebrowser_version_changed = VersionChange.minor
else:
self.qutebrowser_version_changed = VersionChange.major
@ -400,6 +399,13 @@ class YamlMigrations(QObject):
new_name='statusbar.show',
true_value='never',
false_value='always')
self._migrate_renamed_bool(
old_name='content.ssl_strict',
new_name='content.tls.certificate_errors',
true_value='block',
false_value='load-insecurely',
ask_value='ask',
)
for setting in ['colors.webpage.force_dark_color_scheme',
'colors.webpage.prefers_color_scheme_dark']:
@ -519,14 +525,21 @@ class YamlMigrations(QObject):
def _migrate_renamed_bool(self, old_name: str,
new_name: str,
true_value: str,
false_value: str) -> None:
false_value: str,
ask_value: str = None) -> None:
if old_name not in self._settings:
return
self._settings[new_name] = {}
for scope, val in self._settings[old_name].items():
new_value = true_value if val else false_value
if val == 'ask':
assert ask_value is not None
new_value = ask_value
elif val:
new_value = true_value
else:
new_value = false_value
self._settings[new_name][scope] = new_value
del self._settings[old_name]

View File

@ -22,8 +22,11 @@
import os
import sys
import argparse
import pathlib
from typing import Any, Dict, Iterator, List, Optional, Sequence, Tuple
from PyQt5.QtCore import QLibraryInfo, QLocale
from qutebrowser.config import config
from qutebrowser.misc import objects
from qutebrowser.utils import usertypes, qtutils, utils, log, version
@ -154,9 +157,77 @@ def _qtwebengine_features(
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-89740
disabled_features.append('InstalledApp')
if not config.val.input.media_keys:
disabled_features.append('HardwareMediaKeyHandling')
return (enabled_features, disabled_features)
def _get_locale_pak_path(locales_path: pathlib.Path, locale_name: str) -> pathlib.Path:
"""Get the path for a locale .pak file."""
return locales_path / (locale_name + '.pak')
def _get_pak_name(locale_name: str) -> str:
"""Get the Chromium .pak name for a locale name.
Based on Chromium's behavior in l10n_util::CheckAndResolveLocale:
https://source.chromium.org/chromium/chromium/src/+/master:ui/base/l10n/l10n_util.cc;l=344-428;drc=43d5378f7f363dab9271ca37774c71176c9e7b69
"""
if locale_name in {'en', 'en-PH', 'en-LR'}:
return 'en-US'
elif locale_name.startswith('en-'):
return 'en-GB'
elif locale_name.startswith('es-'):
return 'es-419'
elif locale_name == 'pt':
return 'pt-BR'
elif locale_name.startswith('pt-'): # pragma: no cover
return 'pt-PT'
elif locale_name in {'zh-HK', 'zh-MO'}:
return 'zh-TW'
elif locale_name == 'zh' or locale_name.startswith('zh-'):
return 'zh-CN'
return locale_name.split('-')[0]
def _get_lang_override(
webengine_version: utils.VersionNumber,
locale_name: str
) -> Optional[str]:
"""Get a --lang switch to override Qt's locale handling.
This is needed as a WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91715
There is no fix yet, but we assume it'll be fixed with QtWebEngine 5.15.4.
"""
if not config.val.qt.workarounds.locale:
return None
if webengine_version != utils.VersionNumber(5, 15, 3) or not utils.is_linux:
return None
locales_path = pathlib.Path(
QLibraryInfo.location(QLibraryInfo.TranslationsPath)) / 'qtwebengine_locales'
if not locales_path.exists():
log.init.debug(f"{locales_path} not found, skipping workaround!")
return None
pak_path = _get_locale_pak_path(locales_path, locale_name)
if pak_path.exists():
log.init.debug(f"Found {pak_path}, skipping workaround")
return None
pak_name = _get_pak_name(locale_name)
pak_path = _get_locale_pak_path(locales_path, pak_name)
if pak_path.exists():
log.init.debug(f"Found {pak_path}, applying workaround")
return pak_name
log.init.debug(f"Can't find pak in {locales_path} for {locale_name} or {pak_name}")
return 'en-US'
def _qtwebengine_args(
namespace: argparse.Namespace,
special_flags: Sequence[str],
@ -183,6 +254,13 @@ def _qtwebengine_args(
if 'stack' not in namespace.debug_flags:
yield '--disable-in-process-stack-traces'
lang_override = _get_lang_override(
webengine_version=versions.webengine,
locale_name=QLocale().bcp47Name(),
)
if lang_override is not None:
yield f'--lang={lang_override}'
if 'chromium' in namespace.debug_flags:
yield '--enable-logging'
yield '--v=1'

View File

@ -9,7 +9,7 @@ qute://warning/sessions</span> to show it again at a later time.</span>
<p>Since Qt doesn't provide an API to load the history of a tab, qutebrowser relies on a reverse-engineered binary serialization format to load tab history from session files. With Qt 5.15, unfortunately that format changed (due to the underlying Chromium upgrade), in a way which makes it impossible for qutebrowser to load tab history from existing session data.</p>
<p>At the time of writing (January 2021), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a>. However, it unfortunately wasn't ready in time for qutebrowser v2.0.0, as it's a rather big refactoring. It's currently expected to be released as part of qutebrowser v2.1.0.</p>
<p>At the time of writing (January 2021), a new session format which stores part of the needed binary data in saved sessions is <a href="https://github.com/qutebrowser/qutebrowser/issues/5359">in development</a>. However, it unfortunately wasn't ready in time for qutebrowser v2.0.0, as it's a rather big refactoring. It's currently expected to be released in a future v2.x.0 release.</p>
<p>As a stop-gap measure:</p>

View File

@ -648,10 +648,9 @@ class KeySequence:
for key in self._iter_keys():
key_seq = KeySequence(key)
if key_seq in mappings:
new_seq = mappings[key_seq]
assert len(new_seq) == 1
key = new_seq[0].to_int()
keys.append(key)
keys += [info.to_int() for info in mappings[key_seq]]
else:
keys.append(key)
return self.__class__(*keys)
@classmethod

View File

@ -52,7 +52,7 @@ class MacroRecorder:
self._macro_count: Dict[int, int] = {}
self._last_register: Optional[str] = None
@cmdutils.register(instance='macro-recorder', deprecated_name='record-macro')
@cmdutils.register(instance='macro-recorder')
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
def macro_record(self, win_id: int, register: str = None) -> None:
"""Start or stop recording a macro.
@ -77,7 +77,7 @@ class MacroRecorder:
self._macros[register] = []
self._recording_macro = register
@cmdutils.register(instance='macro-recorder', deprecated_name='run-macro')
@cmdutils.register(instance='macro-recorder')
@cmdutils.argument('win_id', value=cmdutils.Value.win_id)
@cmdutils.argument('count', value=cmdutils.Value.count)
def macro_run(self, win_id: int, count: int = 1, register: str = None) -> None:

View File

@ -86,9 +86,10 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
hintmanager = hints.HintManager(win_id, parent=parent)
objreg.register('hintmanager', hintmanager, scope='window',
window=win_id, command_only=True)
modeman.hintmanager = hintmanager
log_sensitive_keys = 'log-sensitive-keys' in objects.debug_flags
keyparsers: ParserDictType = {
usertypes.KeyMode.normal:
modeparsers.NormalKeyParser(
@ -110,7 +111,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
do_log=False,
do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.passthrough:
@ -120,7 +121,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
do_log=False,
do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.command:
@ -130,7 +131,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
do_log=False,
do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.prompt:
@ -140,7 +141,7 @@ def init(win_id: int, parent: QObject) -> 'ModeManager':
commandrunner=commandrunner,
parent=modeman,
passthrough=True,
do_log=False,
do_log=log_sensitive_keys,
supports_count=False),
usertypes.KeyMode.yesno:
@ -385,11 +386,7 @@ class ModeManager(QObject):
self.mode = mode
self.entered.emit(mode, self._win_id)
@cmdutils.register(
instance='mode-manager',
scope='window',
deprecated_name='enter-mode',
)
@cmdutils.register(instance='mode-manager', scope='window')
def mode_enter(self, mode: str) -> None:
"""Enter a key mode.
@ -442,12 +439,8 @@ class ModeManager(QObject):
self.enter(self._prev_mode,
reason='restore mode before {}'.format(mode.name))
@cmdutils.register(
instance='mode-manager',
not_modes=[usertypes.KeyMode.normal],
scope='window',
deprecated_name='leave-mode',
)
@cmdutils.register(instance='mode-manager',
not_modes=[usertypes.KeyMode.normal], scope='window')
def mode_leave(self) -> None:
"""Leave the mode we're currently in."""
if self.mode == usertypes.KeyMode.normal:

View File

@ -86,6 +86,8 @@ class NormalKeyParser(CommandKeyParser):
_partial_timer: Timer to clear partial keypresses.
"""
_sequence: keyutils.KeySequence
def __init__(self, *, win_id: int,
commandrunner: 'runners.CommandRunner',
parent: QObject = None) -> None:
@ -154,6 +156,8 @@ class HintKeyParser(basekeyparser.BaseKeyParser):
_last_press: The nature of the last keypress, a LastPress member.
"""
_sequence: keyutils.KeySequence
def __init__(self, *, win_id: int,
commandrunner: 'runners.CommandRunner',
hintmanager: hints.HintManager,

View File

@ -35,7 +35,7 @@ from qutebrowser.keyinput import modeman
from qutebrowser.mainwindow import tabwidget, mainwindow
from qutebrowser.browser import signalfilter, browsertab, history
from qutebrowser.utils import (log, usertypes, utils, qtutils, objreg,
urlutils, message, jinja)
urlutils, message, jinja, version)
from qutebrowser.misc import quitter
@ -929,26 +929,44 @@ class TabbedBrowser(QWidget):
return
messages = {
browsertab.TerminationStatus.abnormal:
"Renderer process exited with status {}".format(code),
browsertab.TerminationStatus.crashed:
"Renderer process crashed",
browsertab.TerminationStatus.killed:
"Renderer process was killed",
browsertab.TerminationStatus.unknown:
"Renderer process did not start",
browsertab.TerminationStatus.abnormal: "Renderer process exited",
browsertab.TerminationStatus.crashed: "Renderer process crashed",
browsertab.TerminationStatus.killed: "Renderer process was killed",
browsertab.TerminationStatus.unknown: "Renderer process did not start",
}
msg = messages[status]
msg = messages[status] + f" (status {code})"
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-91715
versions = version.qtwebengine_versions()
is_qtbug_91715 = (
status == browsertab.TerminationStatus.unknown and
code == 1002 and
versions.webengine == utils.VersionNumber(5, 15, 3))
def show_error_page(html):
tab.set_html(html)
log.webview.error(msg)
url_string = tab.url(requested=True).toDisplayString()
error_page = jinja.render(
'error.html', title="Error loading {}".format(url_string),
url=url_string, error=msg)
QTimer.singleShot(100, lambda: show_error_page(error_page))
if is_qtbug_91715:
log.webview.error(msg)
log.webview.error('')
log.webview.error(
'NOTE: If you see this and "Network service crashed, restarting '
'service.", please see:')
log.webview.error('https://github.com/qutebrowser/qutebrowser/issues/6235')
log.webview.error(
'You can set the "qt.workarounds.locale" setting in qutebrowser to '
'work around the issue.')
log.webview.error(
'A proper fix is likely available in QtWebEngine soon (which is why '
'the workaround is disabled by default).')
log.webview.error('')
else:
url_string = tab.url(requested=True).toDisplayString()
error_page = jinja.render(
'error.html', title="Error loading {}".format(url_string),
url=url_string, error=msg)
QTimer.singleShot(100, lambda: show_error_page(error_page))
def resizeEvent(self, e):
"""Extend resizeEvent of QWidget to emit a resized signal afterwards.

View File

@ -194,14 +194,6 @@ class _BackendProblemChecker:
sys.exit(usertypes.Exit.err_init)
def _nvidia_shader_workaround(self) -> None:
"""Work around QOpenGLShaderProgram issues.
See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
"""
self._assert_backend(usertypes.Backend.QtWebEngine)
utils.libgl_workaround()
def _xwayland_options(self) -> Tuple[str, List[_Button]]:
"""Get buttons/text for a possible XWayland solution."""
buttons = []
@ -435,7 +427,6 @@ class _BackendProblemChecker:
self._check_backend_modules()
if objects.backend == usertypes.Backend.QtWebEngine:
self._handle_ssl_support()
self._nvidia_shader_workaround()
self._handle_wayland_webgl()
self._handle_cache_nuking()
self._handle_serviceworker_nuking()

View File

@ -359,8 +359,8 @@ class _CrashDialog(QDialog):
Args:
newest: The newest version as a string.
"""
new_version = utils.parse_version(newest)
cur_version = utils.parse_version(qutebrowser.__version__)
new_version = utils.VersionNumber.parse(newest)
cur_version = utils.VersionNumber.parse(qutebrowser.__version__)
lines = ['The report has been sent successfully. Thanks!']
if new_version > cur_version:
lines.append("<b>Note:</b> The newest available version is v{}, "

View File

@ -185,6 +185,11 @@ def check_qt_version():
PYQT_VERSION_STR))
_die(text)
if qt_ver == QVersionNumber(5, 12, 0):
from qutebrowser.utils import log
log.init.warning("Running on Qt 5.12.0. Doing so is unsupported "
"(newer 5.12.x versions are fine).")
def check_ssl_support():
"""Check if SSL support is available."""
@ -274,6 +279,21 @@ def check_optimize_flag():
"unexpected behavior may occur.")
def webengine_early_import():
"""If QtWebEngine is available, import it early.
We need to ensure that QtWebEngine is imported before a QApplication is created for
everything to work properly.
This needs to be done even when using the QtWebKit backend, to ensure that e.g.
error messages in backendproblem.py are accurate.
"""
try:
from PyQt5 import QtWebEngineWidgets # pylint: disable=unused-import
except ImportError:
pass
def early_init(args):
"""Do all needed early initialization.
@ -298,3 +318,4 @@ def early_init(args):
configure_pyqt()
check_ssl_support()
check_optimize_flag()
webengine_early_import()

View File

@ -69,7 +69,7 @@ from typing import IO, ClassVar, Dict, Optional, Tuple, cast
from PyQt5.QtCore import QLibraryInfo
from qutebrowser.utils import log
from qutebrowser.utils import log, version
class ParseError(Exception):
@ -141,7 +141,7 @@ class Ident:
@classmethod
def parse(cls, fobj: IO[bytes]) -> 'Ident':
"""Parse an ELF ident header from a file."""
magic, klass, data, version, osabi, abiversion = _unpack(cls._FORMAT, fobj)
magic, klass, data, elfversion, osabi, abiversion = _unpack(cls._FORMAT, fobj)
try:
bitness = Bitness(klass)
@ -153,7 +153,7 @@ class Ident:
except ValueError:
raise ParseError(f"Invalid endianness {data}")
return cls(magic, bitness, endianness, version, osabi, abiversion)
return cls(magic, bitness, endianness, elfversion, osabi, abiversion)
@dataclasses.dataclass
@ -310,7 +310,11 @@ def _parse_from_file(f: IO[bytes]) -> Versions:
def parse_webenginecore() -> Optional[Versions]:
"""Parse the QtWebEngineCore library file."""
library_path = pathlib.Path(QLibraryInfo.location(QLibraryInfo.LibrariesPath))
if version.is_flatpak():
# Flatpak has Qt in /usr/lib/x86_64-linux-gnu, but QtWebEngine in /app/lib.
library_path = pathlib.Path("/app/lib")
else:
library_path = pathlib.Path(QLibraryInfo.location(QLibraryInfo.LibrariesPath))
# PyQt bundles those files with a .5 suffix
lib_file = library_path / 'libQt5WebEngineCore.so.5'

View File

@ -84,8 +84,27 @@ class GUIProcess(QObject):
if error == QProcess.Crashed and not utils.is_windows:
# Already handled via ExitStatus in _on_finished
return
msg = self._proc.errorString()
message.error("Error while spawning {}: {}".format(self._what, msg))
what = f"{self._what} {self.cmd!r}"
error_descriptions = {
QProcess.FailedToStart: f"{what.capitalize()} failed to start",
QProcess.Crashed: f"{what.capitalize()} crashed",
QProcess.Timedout: f"{what.capitalize()} timed out",
QProcess.WriteError: f"Write error for {what}",
QProcess.WriteError: f"Read error for {what}",
}
error_string = self._proc.errorString()
msg = ': '.join([error_descriptions[error], error_string])
# We can't get some kind of error code from Qt...
# https://bugreports.qt.io/browse/QTBUG-44769
# However, it looks like those strings aren't actually translated?
known_errors = ['No such file or directory', 'Permission denied']
if (': ' in error_string and # pragma: no branch
error_string.split(': ', maxsplit=1)[1] in known_errors):
msg += f'\n(Hint: Make sure {self.cmd!r} exists and is executable)'
message.error(msg)
@pyqtSlot(int, QProcess.ExitStatus)
def _on_finished(self, code, status):

View File

@ -351,13 +351,13 @@ class SqlTable(QObject):
self._name = name
self._create_table(fields, constraints)
def _create_table(self, fields, constraints):
def _create_table(self, fields, constraints, *, force=False):
"""Create the table if the database is uninitialized.
If the table already exists, this does nothing, so it can e.g. be called on
every user_version change.
If the table already exists, this does nothing (except with force=True), so it
can e.g. be called on every user_version change.
"""
if not user_version_changed():
if not user_version_changed() and not force:
return
constraints = constraints or {}

View File

@ -173,6 +173,7 @@ def debug_flag_error(flag):
log-requests: Log all network requests.
log-cookies: Log cookies in cookie filter.
log-scroll-pos: Log all scrolling changes.
log-sensitive-keys: Log keypresses in passthrough modes.
stack: Enable Chromium stack logging.
chromium: Enable Chromium logging.
wait-renderer-process: Wait for debugger in renderer process.
@ -181,7 +182,7 @@ def debug_flag_error(flag):
"""
valid_flags = ['debug-exit', 'pdb-postmortem', 'no-sql-history',
'no-scroll-filtering', 'log-requests', 'log-cookies',
'log-scroll-pos', 'stack', 'chromium',
'log-scroll-pos', 'log-sensitive-keys', 'stack', 'chromium',
'wait-renderer-process', 'avoid-chromium-init', 'werror']
if flag in valid_flags:

View File

@ -31,7 +31,7 @@ import jinja2
import jinja2.nodes
from PyQt5.QtCore import QUrl
from qutebrowser.utils import utils, urlutils, log, qtutils
from qutebrowser.utils import utils, urlutils, log, qtutils, resources
from qutebrowser.misc import debugcachestats
@ -56,7 +56,7 @@ html_fallback = """
class Loader(jinja2.BaseLoader):
"""Jinja loader which uses utils.read_file to load templates.
"""Jinja loader which uses resources.read_file to load templates.
Attributes:
_subdir: The subdirectory to find templates in.
@ -72,7 +72,7 @@ class Loader(jinja2.BaseLoader):
) -> Tuple[str, str, Callable[[], bool]]:
path = os.path.join(self._subdir, template)
try:
source = utils.read_file(path)
source = resources.read_file(path)
except OSError as e:
source = html_fallback.replace("%ERROR%", html.escape(str(e)))
source = source.replace("%FILE%", html.escape(template))
@ -119,7 +119,7 @@ class Environment(jinja2.Environment):
def _data_url(self, path: str) -> str:
"""Get a data: url for the broken qutebrowser logo."""
data = utils.read_file_binary(path)
data = resources.read_file_binary(path)
mimetype = utils.guess_mimetype(path)
return urlutils.data_url(mimetype, data).toString()

View File

@ -98,15 +98,15 @@ def version_check(version: str,
if compiled and exact:
raise ValueError("Can't use compiled=True with exact=True!")
parsed = utils.parse_version(version)
parsed = utils.VersionNumber.parse(version)
op = operator.eq if exact else operator.ge
result = op(utils.parse_version(qVersion()), parsed)
result = op(utils.VersionNumber.parse(qVersion()), parsed)
if compiled and result:
# qVersion() ==/>= parsed, now check if QT_VERSION_STR ==/>= parsed.
result = op(utils.parse_version(QT_VERSION_STR), parsed)
result = op(utils.VersionNumber.parse(QT_VERSION_STR), parsed)
if compiled and result:
# Finally, check PYQT_VERSION_STR as well.
result = op(utils.parse_version(PYQT_VERSION_STR), parsed)
result = op(utils.VersionNumber.parse(PYQT_VERSION_STR), parsed)
return result
@ -116,8 +116,8 @@ MAX_WORLD_ID = 256
def is_new_qtwebkit() -> bool:
"""Check if the given version is a new QtWebKit."""
assert qWebKitVersion is not None
return (utils.parse_version(qWebKitVersion()) >
utils.parse_version('538.1'))
return (utils.VersionNumber.parse(qWebKitVersion()) >
utils.VersionNumber.parse('538.1'))
def is_single_process() -> bool:

View File

@ -0,0 +1,133 @@
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
# Copyright 2014-2021 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
#
# This file is part of qutebrowser.
#
# qutebrowser is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# qutebrowser is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with qutebrowser. If not, see <https://www.gnu.org/licenses/>.
"""Resources related utilities."""
import os.path
import sys
import contextlib
import posixpath
import pathlib
from typing import Iterator, Iterable
# We cannot use the stdlib version on 3.7-3.8 because we need the files() API.
if sys.version_info >= (3, 9):
import importlib.resources as importlib_resources
else: # pragma: no cover
import importlib_resources
import qutebrowser
_cache = {}
def _path(filename: str) -> pathlib.Path:
"""Get a pathlib.Path object for a resource."""
assert not posixpath.isabs(filename), filename
assert os.path.pardir not in filename.split(posixpath.sep), filename
if hasattr(sys, 'frozen'):
# For PyInstaller, where we can't store resource files in a qutebrowser/ folder
# because the executable is already named "qutebrowser" (at least on macOS).
return pathlib.Path(sys.executable).parent / filename
return importlib_resources.files(qutebrowser) / filename
@contextlib.contextmanager
def _keyerror_workaround() -> Iterator[None]:
"""Re-raise KeyErrors as FileNotFoundErrors.
WORKAROUND for zipfile.Path resources raising KeyError when a file was notfound:
https://bugs.python.org/issue43063
Only needed for Python 3.8 and 3.9.
"""
try:
yield
except KeyError as e:
raise FileNotFoundError(str(e))
def _glob(
resource_path: pathlib.Path,
subdir: str,
ext: str,
) -> Iterable[str]:
"""Find resources with the given extension.
Yields a resource name like "html/log.html" (as string).
"""
assert '*' not in ext, ext
assert ext.startswith('.'), ext
glob_path = resource_path / subdir
if isinstance(resource_path, pathlib.Path):
for full_path in glob_path.glob(f'*{ext}'): # . is contained in ext
yield full_path.relative_to(resource_path).as_posix()
else: # zipfile.Path or importlib_resources compat object
# Unfortunately, we can't tell mypy about resource_path being of type
# Union[pathlib.Path, zipfile.Path] because we set "python_version = 3.6" in
# .mypy.ini, but the zipfiel stubs (correctly) only declare zipfile.Path with
# Python 3.8...
assert glob_path.is_dir(), glob_path # type: ignore[unreachable]
for subpath in glob_path.iterdir():
if subpath.name.endswith(ext):
yield posixpath.join(subdir, subpath.name)
def preload() -> None:
"""Load resource files into the cache."""
resource_path = _path('')
for subdir, ext in [
('html', '.html'),
('javascript', '.js'),
('javascript/quirks', '.js'),
]:
for name in _glob(resource_path, subdir, ext):
_cache[name] = read_file(name)
def read_file(filename: str) -> str:
"""Get the contents of a file contained with qutebrowser.
Args:
filename: The filename to open as string.
Return:
The file contents as string.
"""
if filename in _cache:
return _cache[filename]
path = _path(filename)
with _keyerror_workaround():
return path.read_text(encoding='utf-8')
def read_file_binary(filename: str) -> bytes:
"""Get the contents of a binary file contained with qutebrowser.
Args:
filename: The filename to open as string.
Return:
The file contents as a bytes object.
"""
path = _path(filename)
with _keyerror_workaround():
return path.read_bytes()

View File

@ -30,7 +30,7 @@ from typing import Iterator, Optional
from PyQt5.QtCore import QStandardPaths
from PyQt5.QtWidgets import QApplication
from qutebrowser.utils import log, debug, utils
from qutebrowser.utils import log, debug, utils, version
# The cached locations
_locations = {}
@ -232,7 +232,16 @@ def _init_runtime(args: Optional[argparse.Namespace]) -> None:
# Unfortunately this path could get too long for sockets (which have a
# maximum length of 104 chars), so we don't add the username here...
_create(path)
if version.is_flatpak():
# We need a path like /run/user/1000/app/org.qutebrowser.qutebrowser rather than
# /run/user/1000/qutebrowser on Flatpak, since that's bind-mounted in a way that
# it is accessible by any other qutebrowser instances.
*parts, app_name = os.path.split(path)
assert app_name == APPNAME, app_name
path = os.path.join(*parts, 'app', os.environ['FLATPAK_ID'])
else:
_create(path)
_locations[_Location.runtime] = path
@ -314,6 +323,9 @@ def _create(path: str) -> None:
should not be changed.
"""
if APPNAME == 'qute_test' and path.startswith('/home'): # pragma: no cover
for k, v in os.environ.items():
if k == 'HOME' or k.startswith('XDG_'):
log.init.debug(f"{k} = {v}")
raise Exception("Trying to create directory inside /home during "
"tests, this should not happen.")
os.makedirs(path, 0o700, exist_ok=True)

View File

@ -19,10 +19,11 @@
"""Custom useful data types."""
import html
import operator
import enum
import dataclasses
from typing import Any, Optional, Sequence, TypeVar, Union
from typing import Optional, Sequence, TypeVar, Union
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer
from PyQt5.QtCore import QUrl
@ -483,9 +484,6 @@ class AbstractCertificateErrorWrapper:
"""A wrapper over an SSL/certificate error."""
def __init__(self, error: Any) -> None:
self._error = error
def __str__(self) -> str:
raise NotImplementedError
@ -495,6 +493,9 @@ class AbstractCertificateErrorWrapper:
def is_overridable(self) -> bool:
raise NotImplementedError
def html(self) -> str:
return f'<p>{html.escape(str(self))}</p>'
@dataclasses.dataclass
class NavigationRequest:

View File

@ -30,14 +30,11 @@ import datetime
import traceback
import functools
import contextlib
import posixpath
import shlex
import mimetypes
import pathlib
import ctypes
import ctypes.util
from typing import (Any, Callable, IO, Iterator, Optional, Sequence, Tuple, Type, Union,
Iterable, TypeVar, TYPE_CHECKING)
from typing import (Any, Callable, IO, Iterator,
Optional, Sequence, Tuple, Type, Union,
TypeVar, TYPE_CHECKING)
try:
# Protocol was added in Python 3.8
from typing import Protocol
@ -50,11 +47,7 @@ except ImportError: # pragma: no cover
from PyQt5.QtCore import QUrl, QVersionNumber, QRect
from PyQt5.QtGui import QClipboard, QDesktopServices
from PyQt5.QtWidgets import QApplication
# We cannot use the stdlib version on 3.7-3.8 because we need the files() API.
if sys.version_info >= (3, 9):
import importlib.resources as importlib_resources
else: # pragma: no cover
import importlib_resources
import yaml
try:
from yaml import (CSafeLoader as YamlLoader,
@ -65,13 +58,10 @@ except ImportError: # pragma: no cover
SafeDumper as YamlDumper)
YAML_C_EXT = False
import qutebrowser
from qutebrowser.utils import log
fake_clipboard = None
log_clipboard = False
_resource_cache = {}
is_mac = sys.platform.startswith('darwin')
is_linux = sys.platform.startswith('linux')
@ -92,26 +82,74 @@ class Comparable(Protocol):
...
if TYPE_CHECKING:
class VersionNumber(Comparable, QVersionNumber):
class VersionNumber:
"""WORKAROUND for incorrect PyQt stubs."""
else:
class VersionNumber(QVersionNumber):
"""A representation of a version number."""
"""We can't inherit from Protocol and QVersionNumber at runtime."""
def __init__(self, *args: int) -> None:
self._ver = QVersionNumber(*args)
if self._ver.isNull():
raise ValueError("Can't construct a null version")
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
normalized = self.normalized()
if normalized != self:
raise ValueError(
f"Refusing to construct non-normalized version from {args} "
f"(normalized: {tuple(normalized.segments())}).")
normalized = self._ver.normalized()
if normalized != self._ver:
raise ValueError(
f"Refusing to construct non-normalized version from {args} "
f"(normalized: {tuple(normalized.segments())}).")
def __repr__(self):
args = ", ".join(str(s) for s in self.segments())
return f'VersionNumber({args})'
self.major = self._ver.majorVersion()
self.minor = self._ver.minorVersion()
self.patch = self._ver.microVersion()
self.segments = self._ver.segments()
assert len(self.segments) <= 3, self.segments
def __str__(self) -> str:
return ".".join(str(s) for s in self.segments)
def __repr__(self) -> str:
args = ", ".join(str(s) for s in self.segments)
return f'VersionNumber({args})'
def strip_patch(self) -> 'VersionNumber':
"""Get a new VersionNumber with the patch version removed."""
return VersionNumber(*self.segments[:2])
@classmethod
def parse(cls, s: str) -> 'VersionNumber':
"""Parse a version number from a string."""
ver, _suffix = QVersionNumber.fromString(s)
# FIXME: Should we support a suffix?
if ver.isNull():
raise ValueError(f"Failed to parse {s}")
return cls(*ver.normalized().segments())
def __hash__(self) -> int:
return hash(self._ver)
def __eq__(self, other: object) -> bool:
if not isinstance(other, VersionNumber):
return NotImplemented
return self._ver == other._ver
def __ne__(self, other: object) -> bool:
if not isinstance(other, VersionNumber):
return NotImplemented
return self._ver != other._ver
def __ge__(self, other: 'VersionNumber') -> bool:
return self._ver >= other._ver # type: ignore[operator]
def __gt__(self, other: 'VersionNumber') -> bool:
return self._ver > other._ver # type: ignore[operator]
def __le__(self, other: 'VersionNumber') -> bool:
return self._ver <= other._ver # type: ignore[operator]
def __lt__(self, other: 'VersionNumber') -> bool:
return self._ver < other._ver # type: ignore[operator]
class Unreachable(Exception):
@ -196,110 +234,6 @@ def compact_text(text: str, elidelength: int = None) -> str:
return out
def _resource_path(filename: str) -> pathlib.Path:
"""Get a pathlib.Path object for a resource."""
assert not posixpath.isabs(filename), filename
assert os.path.pardir not in filename.split(posixpath.sep), filename
if hasattr(sys, 'frozen'):
# For PyInstaller, where we can't store resource files in a qutebrowser/ folder
# because the executable is already named "qutebrowser" (at least on macOS).
return pathlib.Path(sys.executable).parent / filename
return importlib_resources.files(qutebrowser) / filename
@contextlib.contextmanager
def _resource_keyerror_workaround() -> Iterator[None]:
"""Re-raise KeyErrors as FileNotFoundErrors.
WORKAROUND for zipfile.Path resources raising KeyError when a file was notfound:
https://bugs.python.org/issue43063
Only needed for Python 3.8 and 3.9.
"""
try:
yield
except KeyError as e:
raise FileNotFoundError(str(e))
def _glob_resources(
resource_path: pathlib.Path,
subdir: str,
ext: str,
) -> Iterable[str]:
"""Find resources with the given extension.
Yields a resource name like "html/log.html" (as string).
"""
assert '*' not in ext, ext
assert ext.startswith('.'), ext
path = resource_path / subdir
if isinstance(resource_path, pathlib.Path):
for full_path in path.glob(f'*{ext}'): # . is contained in ext
yield full_path.relative_to(resource_path).as_posix()
else: # zipfile.Path or importlib_resources compat object
# Unfortunately, we can't tell mypy about resource_path being of type
# Union[pathlib.Path, zipfile.Path] because we set "python_version = 3.6" in
# .mypy.ini, but the zipfiel stubs (correctly) only declare zipfile.Path with
# Python 3.8...
assert path.is_dir(), path # type: ignore[unreachable]
for subpath in path.iterdir():
if subpath.name.endswith(ext):
yield posixpath.join(subdir, subpath.name)
def preload_resources() -> None:
"""Load resource files into the cache."""
resource_path = _resource_path('')
for subdir, ext in [
('html', '.html'),
('javascript', '.js'),
('javascript/quirks', '.js'),
]:
for name in _glob_resources(resource_path, subdir, ext):
_resource_cache[name] = read_file(name)
def read_file(filename: str) -> str:
"""Get the contents of a file contained with qutebrowser.
Args:
filename: The filename to open as string.
Return:
The file contents as string.
"""
if filename in _resource_cache:
return _resource_cache[filename]
path = _resource_path(filename)
with _resource_keyerror_workaround():
return path.read_text(encoding='utf-8')
def read_file_binary(filename: str) -> bytes:
"""Get the contents of a binary file contained with qutebrowser.
Args:
filename: The filename to open as string.
Return:
The file contents as a bytes object.
"""
path = _resource_path(filename)
with _resource_keyerror_workaround():
return path.read_bytes()
def parse_version(version: str) -> VersionNumber:
"""Parse a version string."""
ver, _suffix = QVersionNumber.fromString(version)
return VersionNumber(ver.normalized())
def format_seconds(total_seconds: int) -> str:
"""Format a count of seconds to get a [H:]M:SS string."""
prefix = '-' if total_seconds < 0 else ''
@ -671,7 +605,7 @@ def open_file(filename: str, cmdline: str = None) -> None:
# if we want to use the default
override = config.val.downloads.open_dispatcher
if version.is_sandboxed():
if version.is_flatpak():
if cmdline:
message.error("Cannot spawn download dispatcher from sandbox")
return
@ -817,19 +751,6 @@ def ceil_log(number: int, base: int) -> int:
return result
def libgl_workaround() -> None:
"""Work around QOpenGLShaderProgram issues, especially for Nvidia.
See https://bugs.launchpad.net/ubuntu/+source/python-qt4/+bug/941826
"""
if os.environ.get('QUTE_SKIP_LIBGL_WORKAROUND'):
return
libgl = ctypes.util.find_library("GL")
if libgl is not None: # pragma: no branch
ctypes.CDLL(libgl, mode=ctypes.RTLD_GLOBAL)
def parse_duration(duration: str) -> int:
"""Parse duration in format XhYmZs into milliseconds duration."""
if duration.isdigit():

View File

@ -53,17 +53,11 @@ except ImportError: # pragma: no cover
import qutebrowser
from qutebrowser.utils import log, utils, standarddir, usertypes, message
from qutebrowser.utils import log, utils, standarddir, usertypes, message, resources
from qutebrowser.misc import objects, earlyinit, sql, httpclient, pastebin, elf
from qutebrowser.browser import pdfjs
from qutebrowser.config import config, websettings
try:
from qutebrowser.browser.webengine import webenginesettings
except ImportError: # pragma: no cover
webenginesettings = None # type: ignore[assignment]
_LOGO = r'''
______ ,,
,.-"` | ,-` |
@ -160,7 +154,7 @@ def distribution() -> Optional[DistributionInfo]:
dist_version: Optional[utils.VersionNumber] = None
for version_key in ['VERSION', 'VERSION_ID']:
if version_key in info:
dist_version = utils.parse_version(info[version_key])
dist_version = utils.VersionNumber.parse(info[version_key])
break
dist_id = info.get('ID', None)
@ -189,8 +183,12 @@ def distribution() -> Optional[DistributionInfo]:
parsed=parsed, version=dist_version, pretty=pretty, id=dist_id)
def is_sandboxed() -> bool:
"""Whether the environment has restricted access to the host system."""
def is_flatpak() -> bool:
"""Whether qutebrowser is running via Flatpak.
If packaged via Flatpak, the environment is has restricted access to the host
system.
"""
current_distro = distribution()
if current_distro is None:
return False
@ -218,7 +216,7 @@ def _git_str() -> Optional[str]:
return commit
# If that fails, check the git-commit-id file.
try:
return utils.read_file('git-commit-id')
return resources.read_file('git-commit-id')
except (OSError, ImportError):
return None
@ -492,6 +490,9 @@ def _get_pyqt_webengine_qt_version() -> Optional[str]:
https://www.riverbankcomputing.com/pipermail/pyqt/2021-February/043591.html
https://www.riverbankcomputing.com/pipermail/pyqt/2021-February/043638.html
PyQtWebEngine 5.15.4 renamed it to PyQtWebEngine-Qt5...:
https://www.riverbankcomputing.com/pipermail/pyqt/2021-March/043699.html
Here, we try to use importlib.metadata or its backport (optional dependency) to
figure out that version number. If PyQtWebEngine is installed via pip, this will
give us an accurate answer.
@ -505,11 +506,13 @@ def _get_pyqt_webengine_qt_version() -> Optional[str]:
log.misc.debug("Neither importlib.metadata nor backport available")
return None
try:
return importlib_metadata.version('PyQtWebEngine-Qt')
except importlib_metadata.PackageNotFoundError:
log.misc.debug("PyQtWebEngine-Qt not found")
return None
for suffix in ['Qt5', 'Qt']:
try:
return importlib_metadata.version(f'PyQtWebEngine-{suffix}')
except importlib_metadata.PackageNotFoundError:
log.misc.debug(f"PyQtWebEngine-{suffix} not found")
return None
@dataclasses.dataclass
@ -520,8 +523,9 @@ class WebEngineVersions:
webengine: utils.VersionNumber
chromium: Optional[str]
source: str
chromium_major: Optional[int] = dataclasses.field(init=False)
_CHROMIUM_VERSIONS: ClassVar[Dict[str, str]] = {
_CHROMIUM_VERSIONS: ClassVar[Dict[utils.VersionNumber, str]] = {
# Qt 5.12: Chromium 69
# (LTS) 69.0.3497.128 (~2018-09-11)
# 5.12.0: Security fixes up to 70.0.3538.102 (~2018-10-24)
@ -535,21 +539,21 @@ class WebEngineVersions:
# 5.12.8: Security fixes up to 80.0.3987.149 (2020-03-18)
# 5.12.9: Security fixes up to 83.0.4103.97 (2020-06-03)
# 5.12.10: Security fixes up to 86.0.4240.75 (2020-10-06)
'5.12': '69.0.3497.128',
utils.VersionNumber(5, 12): '69.0.3497.128',
# Qt 5.13: Chromium 73
# 73.0.3683.105 (~2019-02-28)
# 5.13.0: Security fixes up to 74.0.3729.157 (2019-05-14)
# 5.13.1: Security fixes up to 76.0.3809.87 (2019-07-30)
# 5.13.2: Security fixes up to 77.0.3865.120 (2019-10-10)
'5.13': '73.0.3683.105',
utils.VersionNumber(5, 13): '73.0.3683.105',
# Qt 5.14: Chromium 77
# 77.0.3865.129 (~2019-10-10)
# 5.14.0: Security fixes up to 77.0.3865.129 (~2019-09-10)
# 5.14.1: Security fixes up to 79.0.3945.117 (2020-01-07)
# 5.14.2: Security fixes up to 80.0.3987.132 (2020-03-03)
'5.14': '77.0.3865.129',
utils.VersionNumber(5, 14): '77.0.3865.129',
# Qt 5.15: Chromium 80
# 80.0.3987.163 (2020-04-02)
@ -557,13 +561,22 @@ class WebEngineVersions:
# 5.15.1: Security fixes up to 85.0.4183.83 (2020-08-25)
# 5.15.2: Updated to 83.0.4103.122 (~2020-06-24)
# Security fixes up to 86.0.4240.183 (2020-11-02)
'5.15': '80.0.3987.163',
'5.15.2': '83.0.4103.122',
'5.15.3': '87.0.4280.144',
# 5.15.3: Updated to 87.0.4280.144 (~2020-12-02)
# Security fixes up to 88.0.4324.150 (2021-02-04)
utils.VersionNumber(5, 15): '80.0.3987.163',
utils.VersionNumber(5, 15, 2): '83.0.4103.122',
utils.VersionNumber(5, 15, 3): '87.0.4280.144',
}
def __post_init__(self) -> None:
"""Set the major Chromium version."""
if self.chromium is None:
self.chromium_major = None
else:
self.chromium_major = int(self.chromium.split('.')[0])
def __str__(self) -> str:
s = f'QtWebEngine {self.webengine.toString()}'
s = f'QtWebEngine {self.webengine}'
if self.chromium is not None:
s += f', Chromium {self.chromium}'
if self.source != 'UA':
@ -580,7 +593,7 @@ class WebEngineVersions:
"""
assert ua.qt_version is not None, ua
return cls(
webengine=utils.parse_version(ua.qt_version),
webengine=utils.VersionNumber.parse(ua.qt_version),
chromium=ua.upstream_browser_version,
source='UA',
)
@ -597,19 +610,30 @@ class WebEngineVersions:
(though hackish) way to get a more accurate result.
"""
return cls(
webengine=utils.parse_version(versions.webengine),
webengine=utils.VersionNumber.parse(versions.webengine),
chromium=versions.chromium,
source='ELF',
)
@classmethod
def _infer_chromium_version(cls, pyqt_webengine_version: str) -> Optional[str]:
def _infer_chromium_version(
cls,
pyqt_webengine_version: utils.VersionNumber,
) -> Optional[str]:
"""Infer the Chromium version based on the PyQtWebEngine version."""
chromium_version = cls._CHROMIUM_VERSIONS.get(pyqt_webengine_version)
if chromium_version is not None:
return chromium_version
# 5.14.2 -> 5.14
minor_version = pyqt_webengine_version.rsplit('.', maxsplit=1)[0]
# 5.15 patch versions change their QtWebEngine version, but no changes are
# expected after 5.15.3.
v5_15_3 = utils.VersionNumber(5, 15, 3)
if v5_15_3 <= pyqt_webengine_version < utils.VersionNumber(6):
minor_version = v5_15_3
else:
# e.g. 5.14.2 -> 5.14
minor_version = pyqt_webengine_version.strip_patch()
return cls._CHROMIUM_VERSIONS.get(minor_version)
@classmethod
@ -631,9 +655,10 @@ class WebEngineVersions:
Note that we only can get the PyQtWebEngine version with PyQt 5.13 or newer.
With Qt 5.12, we instead rely on qVersion().
"""
parsed = utils.VersionNumber.parse(pyqt_webengine_version)
return cls(
webengine=utils.parse_version(pyqt_webengine_version),
chromium=cls._infer_chromium_version(pyqt_webengine_version),
webengine=parsed,
chromium=cls._infer_chromium_version(parsed),
source=source,
)
@ -657,7 +682,7 @@ def qtwebengine_versions(avoid_init: bool = False) -> WebEngineVersions:
- https://www.chromium.org/developers/calendar
- https://chromereleases.googleblog.com/
"""
assert webenginesettings is not None
from qutebrowser.browser.webengine import webenginesettings
if webenginesettings.parsed_user_agent is None and not avoid_init:
webenginesettings.init_user_agent()
@ -855,9 +880,6 @@ def opengl_info() -> Optional[OpenGLInfo]: # pragma: no cover
"""
assert QApplication.instance()
# Some setups can segfault in here if we don't do this.
utils.libgl_workaround()
override = os.environ.get('QUTE_FAKE_OPENGL')
if override is not None:
log.init.debug("Using override {}".format(override))

View File

@ -3,11 +3,11 @@
adblock==0.4.2 ; python_version!="3.10"
colorama==0.4.4
dataclasses==0.6 ; python_version<"3.7"
importlib-metadata==3.7.0 ; python_version<"3.8"
importlib-resources==5.1.1 ; python_version<"3.9"
importlib-metadata==3.7.3 ; python_version<"3.8"
importlib-resources==5.1.2 ; python_version<"3.9"
Jinja2==2.11.3
MarkupSafe==1.1.1
Pygments==2.8.0
Pygments==2.8.1
PyYAML==5.4.1
typing-extensions==3.7.4.3
zipp==3.4.0
zipp==3.4.1

View File

@ -187,6 +187,8 @@ PERFECT_FILES = [
'qutebrowser/utils/usertypes.py'),
('tests/unit/utils/test_utils.py',
'qutebrowser/utils/utils.py'),
('tests/unit/utils/test_resources.py',
'qutebrowser/utils/resources.py'),
('tests/unit/utils/test_version.py',
'qutebrowser/utils/version.py'),
('tests/unit/utils/test_debug.py',

View File

@ -1,12 +1,5 @@
FROM archlinux:latest
# WORKAROUND for glibc 2.33 and old Docker
# See https://github.com/actions/virtual-environments/issues/2658
# Thanks to https://github.com/lxqt/lxqt-panel/pull/1562
RUN patched_glibc=glibc-linux4-2.33-4-x86_64.pkg.tar.zst && \
curl -LO "https://repo.archlinuxcn.org/x86_64/$patched_glibc" && \
bsdtar -C / -xvf "$patched_glibc"
{% if unstable %}
RUN sed -i '/^# after the header/a[kde-unstable]\nInclude = /etc/pacman.d/mirrorlist\n\n[testing]\nInclude = /etc/pacman.d/mirrorlist' /etc/pacman.conf
{% endif %}

View File

@ -155,7 +155,7 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]:
"""Check commonly misspelled words."""
# Words which I often misspell
words = {'behaviour', 'quitted', 'likelyhood', 'sucessfully',
'occur[^rs .!]', 'seperator', 'explicitely', 'auxillary',
'occur[^rs .!,]', 'seperator', 'explicitely', 'auxillary',
'accidentaly', 'ambigious', 'loosly', 'initialis', 'convienence',
'similiar', 'uncommited', 'reproducable', 'an user',
'convienience', 'wether', 'programatically', 'splitted',
@ -247,7 +247,23 @@ def check_spelling(args: argparse.Namespace) -> Optional[bool]:
(
re.compile(fr'qtbot\.(?!{qtbot_excludes})[a-z]+[A-Z].*'),
"use snake-case instead",
)
),
(
re.compile(r'\.joinpath\((?!\*)'),
"use the / operator for joining paths",
),
(
re.compile(r"""pathlib\.Path\(["']~["']\)\.expanduser\(\)"""),
"use pathlib.Path.home() instead",
),
(
re.compile(r'pathlib\.Path\(tmp_path\)'),
"tmp_path already is a pathlib.Path",
),
(
re.compile(r'pathlib\.Path\(tmpdir\)'),
"use tmp_path instead",
),
]
# Files which should be ignored, e.g. because they come from another

View File

@ -138,8 +138,10 @@ CHANGELOG_URLS = {
'toml': 'https://github.com/uiri/toml/releases',
'PyQt5': 'https://www.riverbankcomputing.com/news',
'PyQt5-Qt': 'https://www.riverbankcomputing.com/news',
'PyQt5-Qt5': 'https://www.riverbankcomputing.com/news',
'PyQtWebEngine': 'https://www.riverbankcomputing.com/news',
'PyQtWebEngine-Qt': 'https://www.riverbankcomputing.com/news',
'PyQtWebEngine-Qt5': 'https://www.riverbankcomputing.com/news',
'PyQt-builder': 'https://www.riverbankcomputing.com/news',
'PyQt5-sip': 'https://www.riverbankcomputing.com/news',
'PyQt5-stubs': 'https://github.com/stlehmann/PyQt5-stubs/blob/master/CHANGELOG.md',
@ -279,7 +281,7 @@ def run_pip(venv_dir, *args, quiet=False, **kwargs):
arg_str = ' '.join(str(arg) for arg in args)
utils.print_col('venv$ pip {}'.format(arg_str), 'blue')
venv_python = os.path.join(venv_dir, 'bin', 'python')
venv_python = get_venv_python(venv_dir)
return subprocess.run([venv_python, '-m', 'pip'] + args, check=True, **kwargs)
@ -399,7 +401,13 @@ def _get_changes(diff):
for line in diff:
if not line.startswith('-') and not line.startswith('+'):
continue
if line.startswith('+++ ') or line.startswith('--- '):
elif line.startswith('+++ ') or line.startswith('--- '):
continue
elif not line.strip():
# Could be newline changes on Windows
continue
elif line[1:].startswith('# This file is automatically'):
# Could be newline changes on Windows
continue
name, version = parse_versioned_line(line[1:])
@ -458,6 +466,12 @@ def get_host_python(name):
return sys.executable
def get_venv_python(venv_dir):
"""Get the path to Python inside a virtualenv."""
subdir = 'Scripts' if os.name == 'nt' else 'bin'
return os.path.join(venv_dir, subdir, 'python')
def get_outfile(name):
"""Get the path to the output requirements.txt file."""
if name == 'qutebrowser':
@ -510,7 +524,7 @@ def test_tox():
with tempfile.TemporaryDirectory() as tmpdir:
venv_dir = os.path.join(tmpdir, 'venv')
tox_workdir = os.path.join(tmpdir, 'tox-workdir')
venv_python = os.path.join(venv_dir, 'bin', 'python')
venv_python = get_venv_python(venv_dir)
init_venv(host_python, venv_dir, req_path)
list_proc = subprocess.run([venv_python, '-m', 'tox', '--listenvs'],
check=True,

View File

@ -58,6 +58,7 @@ def main():
'protected-access',
'len-as-condition',
'compare-to-empty-string',
'pointless-statement',
# directories without __init__.py...
'import-error',
]

View File

@ -447,6 +447,7 @@ def _generate_setting_option(f, opt):
f.write("\nThis setting supports URL patterns.\n")
if opt.no_autoconfig:
f.write("\nThis setting can only be set in config.py.\n")
_generate_setting_backend_info(f, opt)
f.write("\n")
typ = opt.typ.get_name().replace(',', '&#44;')
f.write('Type: <<types,{typ}>>\n'.format(typ=typ))
@ -465,7 +466,6 @@ def _generate_setting_option(f, opt):
f.write("\n")
f.write("Default: {}\n".format(opt.typ.to_doc(opt.default)))
_generate_setting_backend_info(f, opt)
def generate_settings(filename):

View File

@ -50,6 +50,9 @@ for ua_string in reversed(response.json()):
continue
if any(part.startswith("OPR/") or part.startswith("Edg/") for part in parts):
continue
if 'Chrome/99.0.7113.93' in parts:
# Fake or false-positive entry
continue
user_agent = qutebrowser.config.websettings.UserAgent.parse(ua_string)

View File

@ -82,7 +82,7 @@ if __name__ == "__main__":
.format(v=version))
print("* Windows: git fetch; git checkout v{v}; "
"py -3.9 -m tox -e build-release -- --asciidoc "
"$env:userprofile\\bin\\asciidoc-9.0.5\\asciidoc.py --upload"
"$env:userprofile\\bin\\asciidoc-9.1.0\\asciidoc.py --upload"
.format(v=version))
print("* macOS: git fetch && git checkout v{v} && "
"tox -e build-release -- --upload"

View File

@ -135,7 +135,8 @@ def insert_qb(history, dest):
'INSERT INTO History (url,title,atime,redirect) VALUES (?,?,?,?)',
history
)
cursor.execute('DROP TABLE CompletionHistory')
cursor.execute('UPDATE CompletionMetaInfo SET value = 1 '
'WHERE key = "force_rebuild"')
conn.commit()
conn.close()

View File

@ -99,6 +99,18 @@ def parse_args(argv: List[str] = None) -> argparse.Namespace:
return parser.parse_args(argv)
def _version_key(v):
"""Sort PyQt requirement file prefixes.
If we have a filename like requirements-pyqt-pyinstaller.txt, that should
always be sorted after all others (hence we return a "999" key).
"""
try:
return tuple(int(v) for c in v.split('.'))
except ValueError:
return 999
def pyqt_versions() -> List[str]:
"""Get a list of all available PyQt versions.
@ -110,8 +122,7 @@ def pyqt_versions() -> List[str]:
for req in requirements_dir.glob('requirements-pyqt-*.txt'):
version_set.add(req.stem.split('-')[-1])
versions = sorted(version_set,
key=lambda v: [int(c) for c in v.split('.')])
versions = sorted(version_set, key=_version_key)
return versions + ['auto']

View File

@ -22,9 +22,9 @@
"""The qutebrowser test suite conftest file."""
import os
import pathlib
import sys
import warnings
import pathlib
import pytest
import hypothesis
@ -109,12 +109,6 @@ def _apply_platform_markers(config, item):
pytest.mark.skipif,
sys.getfilesystemencoding() == 'ascii',
"Skipped because of ASCII locale"),
('qtwebkit6021_xfail',
pytest.mark.xfail,
version.qWebKitVersion and # type: ignore[unreachable]
version.qWebKitVersion() == '602.1',
"Broken on WebKit 602.1")
]
for searched_marker, new_marker_kind, condition, default_reason in markers:
@ -189,9 +183,10 @@ def pytest_collection_modifyitems(config, items):
def pytest_ignore_collect(path):
"""Ignore BDD tests if we're unable to run them."""
fspath = pathlib.Path(path)
skip_bdd = hasattr(sys, 'frozen')
rel_path = path.relto(os.path.dirname(__file__))
return rel_path == os.path.join('end2end', 'features') and skip_bdd
rel_path = fspath.relative_to(pathlib.Path(__file__).parent)
return rel_path == pathlib.Path('end2end') / 'features' and skip_bdd
@pytest.fixture(scope='session')
@ -245,12 +240,6 @@ def set_backend(monkeypatch, request):
monkeypatch.setattr(objects, 'backend', backend)
@pytest.fixture(autouse=True, scope='session')
def apply_libgl_workaround():
"""Make sure we load libGL early so QtWebEngine tests run properly."""
utils.libgl_workaround()
@pytest.fixture(autouse=True)
def apply_fake_os(monkeypatch, request):
fake_os = request.node.get_closest_marker('fake_os')

View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>MathML-like SVG</title>
</head>
<body style="margin: 0; background-color: #ffff99">
<!--
Image based on: https://en.wikipedia.org/wiki/Pythagorean_theorem
with a black square added for testing.
onload based on:
https://stackoverflow.com/questions/53423742/waiting-for-an-image-to-finish-rendering
-->
<img
class="mwe-math-fallback-image-inline"
src="mathml.svg"
alt="Pythagorean theorem"
onload="requestAnimationFrame(() => requestAnimationFrame(() => console.log('Image loaded')));"
>
<!-- -->
</body>
</html>

View File

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="12.983ex"
height="2.843ex"
style="vertical-align: -0.505ex;"
viewBox="0 -1006.6 5589.7 1223.9"
role="img"
focusable="false"
aria-labelledby="MathJax-SVG-1-Title"
version="1.1"
id="svg36"
sodipodi:docname="mathml.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
<metadata
id="metadata40">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title>{\displaystyle a^{2}+b^{2}=c^{2}.}</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="944"
inkscape:window-height="1036"
id="namedview38"
showgrid="false"
inkscape:zoom="0.27282322"
inkscape:cx="1686.0735"
inkscape:cy="602.78657"
inkscape:window-x="964"
inkscape:window-y="22"
inkscape:window-maximized="0"
inkscape:current-layer="svg36" />
<title
id="MathJax-SVG-1-Title">{\displaystyle a^{2}+b^{2}=c^{2}.}</title>
<defs
aria-hidden="true"
id="defs10">
<path
stroke-width="1"
id="E1-MJMATHI-61"
d="M33 157Q33 258 109 349T280 441Q331 441 370 392Q386 422 416 422Q429 422 439 414T449 394Q449 381 412 234T374 68Q374 43 381 35T402 26Q411 27 422 35Q443 55 463 131Q469 151 473 152Q475 153 483 153H487Q506 153 506 144Q506 138 501 117T481 63T449 13Q436 0 417 -8Q409 -10 393 -10Q359 -10 336 5T306 36L300 51Q299 52 296 50Q294 48 292 46Q233 -10 172 -10Q117 -10 75 30T33 157ZM351 328Q351 334 346 350T323 385T277 405Q242 405 210 374T160 293Q131 214 119 129Q119 126 119 118T118 106Q118 61 136 44T179 26Q217 26 254 59T298 110Q300 114 325 217T351 328Z" />
<path
stroke-width="1"
id="E1-MJMAIN-32"
d="M109 429Q82 429 66 447T50 491Q50 562 103 614T235 666Q326 666 387 610T449 465Q449 422 429 383T381 315T301 241Q265 210 201 149L142 93L218 92Q375 92 385 97Q392 99 409 186V189H449V186Q448 183 436 95T421 3V0H50V19V31Q50 38 56 46T86 81Q115 113 136 137Q145 147 170 174T204 211T233 244T261 278T284 308T305 340T320 369T333 401T340 431T343 464Q343 527 309 573T212 619Q179 619 154 602T119 569T109 550Q109 549 114 549Q132 549 151 535T170 489Q170 464 154 447T109 429Z" />
<path
stroke-width="1"
id="E1-MJMAIN-2B"
d="M56 237T56 250T70 270H369V420L370 570Q380 583 389 583Q402 583 409 568V270H707Q722 262 722 250T707 230H409V-68Q401 -82 391 -82H389H387Q375 -82 369 -68V230H70Q56 237 56 250Z" />
<path
stroke-width="1"
id="E1-MJMATHI-62"
d="M73 647Q73 657 77 670T89 683Q90 683 161 688T234 694Q246 694 246 685T212 542Q204 508 195 472T180 418L176 399Q176 396 182 402Q231 442 283 442Q345 442 383 396T422 280Q422 169 343 79T173 -11Q123 -11 82 27T40 150V159Q40 180 48 217T97 414Q147 611 147 623T109 637Q104 637 101 637H96Q86 637 83 637T76 640T73 647ZM336 325V331Q336 405 275 405Q258 405 240 397T207 376T181 352T163 330L157 322L136 236Q114 150 114 114Q114 66 138 42Q154 26 178 26Q211 26 245 58Q270 81 285 114T318 219Q336 291 336 325Z" />
<path
stroke-width="1"
id="E1-MJMAIN-3D"
d="M56 347Q56 360 70 367H707Q722 359 722 347Q722 336 708 328L390 327H72Q56 332 56 347ZM56 153Q56 168 72 173H708Q722 163 722 153Q722 140 707 133H70Q56 140 56 153Z" />
<path
stroke-width="1"
id="E1-MJMATHI-63"
d="M34 159Q34 268 120 355T306 442Q362 442 394 418T427 355Q427 326 408 306T360 285Q341 285 330 295T319 325T330 359T352 380T366 386H367Q367 388 361 392T340 400T306 404Q276 404 249 390Q228 381 206 359Q162 315 142 235T121 119Q121 73 147 50Q169 26 205 26H209Q321 26 394 111Q403 121 406 121Q410 121 419 112T429 98T420 83T391 55T346 25T282 0T202 -11Q127 -11 81 37T34 159Z" />
<path
stroke-width="1"
id="E1-MJMAIN-2E"
d="M78 60Q78 84 95 102T138 120Q162 120 180 104T199 61Q199 36 182 18T139 0T96 17T78 60Z" />
</defs>
<g
stroke="currentColor"
fill="currentColor"
stroke-width="0"
transform="matrix(1 0 0 -1 0 0)"
aria-hidden="true"
id="g34">
<use
xlink:href="#E1-MJMATHI-61"
x="0"
y="0"
id="use12" />
<use
transform="scale(0.707)"
xlink:href="#E1-MJMAIN-32"
x="748"
y="583"
id="use14" />
<use
xlink:href="#E1-MJMAIN-2B"
x="1205"
y="0"
id="use16" />
<g
transform="translate(2206,0)"
id="g22">
<use
xlink:href="#E1-MJMATHI-62"
x="0"
y="0"
id="use18" />
<use
transform="scale(0.707)"
xlink:href="#E1-MJMAIN-32"
x="607"
y="583"
id="use20" />
</g>
<use
xlink:href="#E1-MJMAIN-3D"
x="3367"
y="0"
id="use24" />
<g
transform="translate(4423,0)"
id="g30">
<use
xlink:href="#E1-MJMATHI-63"
x="0"
y="0"
id="use26" />
<use
transform="scale(0.707)"
xlink:href="#E1-MJMAIN-32"
x="613"
y="583"
id="use28" />
</g>
<use
xlink:href="#E1-MJMAIN-2E"
x="5311"
y="0"
id="use32" />
</g>
<rect
style="fill:#000000"
id="rect865"
width="338.88928"
height="316.48901"
x="2.5373409"
y="-1004.8583" />
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@ -0,0 +1,5 @@
document.addEventListener('DOMContentLoaded', (event) => {
const elem = document.getElementById('text');
elem.textContent = 'Script loaded';
console.log('Script loaded');
})

View File

@ -140,7 +140,7 @@ Feature: Various utility commands.
Scenario: :jseval --file using a file that doesn't exist as js-code
When I run :jseval --file /nonexistentfile
Then the error "[Errno 2] No such file or directory: '/nonexistentfile'" should be shown
Then the error "[Errno 2] *: '/nonexistentfile'" should be shown
And "No output or error" should not be logged
# :debug-webaction
@ -528,13 +528,13 @@ Feature: Various utility commands.
@qtwebkit_skip @no_invalid_lines @posix
Scenario: Renderer crash
When I run :open -t chrome://crash
Then "Renderer process crashed" should be logged
Then "Renderer process crashed (status *)" should be logged
And "* 'Error loading chrome://crash/'" should be logged
@qtwebkit_skip @no_invalid_lines @flaky
Scenario: Renderer kill
When I run :open -t chrome://kill
Then "Renderer process was killed" should be logged
Then "Renderer process was killed (status *)" should be logged
And "* 'Error loading chrome://kill/'" should be logged
# https://github.com/qutebrowser/qutebrowser/issues/2290
@ -544,7 +544,7 @@ Feature: Various utility commands.
And I open data/numbers/1.txt
And I open data/numbers/2.txt in a new tab
And I run :open chrome://kill
And I wait for "Renderer process was killed" in the log
And I wait for "Renderer process was killed (status *)" in the log
And I open data/numbers/3.txt
Then no crash should happen
@ -554,11 +554,11 @@ Feature: Various utility commands.
When I open data/crashers/webrtc.html in a new tab
And I run :reload
And I wait until data/crashers/webrtc.html is loaded
Then "Renderer process crashed" should not be logged
Then "Renderer process crashed (status *)" should not be logged
Scenario: InstalledApps crash
When I open data/crashers/installedapp.html in a new tab
Then "Renderer process was killed" should not be logged
Then "Renderer process was killed (status *)" should not be logged
## Other

View File

@ -163,45 +163,83 @@ Feature: Prompts
# SSL
Scenario: SSL error with content.ssl_strict = false
Scenario: SSL error with content.tls.certificate_errors = load-insecurely
When I clear SSL errors
And I set content.ssl_strict to false
And I set content.tls.certificate_errors to load-insecurely
And I load an SSL page
And I wait until the SSL page finished loading
Then the error "Certificate error: *" should be shown
And the page should contain the plaintext "Hello World via SSL!"
Scenario: SSL error with content.ssl_strict = true
Scenario: SSL error with content.tls.certificate_errors = block
When I clear SSL errors
And I set content.ssl_strict to true
And I set content.tls.certificate_errors to block
And I load an SSL page
Then a SSL error page should be shown
Scenario: SSL error with content.ssl_strict = ask -> yes
Scenario: SSL error with content.tls.certificate_errors = ask -> yes
When I clear SSL errors
And I set content.ssl_strict to ask
And I set content.tls.certificate_errors to ask
And I load an SSL page
And I wait for a prompt
And I run :prompt-accept yes
And I wait until the SSL page finished loading
Then the page should contain the plaintext "Hello World via SSL!"
Scenario: SSL error with content.ssl_strict = ask -> no
Scenario: SSL error with content.tls.certificate_errors = ask -> no
When I clear SSL errors
And I set content.ssl_strict to ask
And I set content.tls.certificate_errors to ask
And I load an SSL page
And I wait for a prompt
And I run :prompt-accept no
Then a SSL error page should be shown
Scenario: SSL error with content.ssl_strict = ask -> abort
Scenario: SSL error with content.tls.certificate_errors = ask -> abort
When I clear SSL errors
And I set content.ssl_strict to ask
And I set content.tls.certificate_errors to ask
And I load an SSL page
And I wait for a prompt
And I run :mode-leave
Then a SSL error page should be shown
Scenario: SSL error with content.tls.certificate_errors = ask-block-thirdparty -> yes
When I clear SSL errors
And I set content.tls.certificate_errors to ask-block-thirdparty
And I load an SSL page
And I wait for a prompt
And I run :prompt-accept yes
And I wait until the SSL page finished loading
Then the page should contain the plaintext "Hello World via SSL!"
Scenario: SSL resource error with content.tls.certificate_errors = ask -> yes
When I clear SSL errors
And I set content.tls.certificate_errors to ask
And I load an SSL resource page
And I wait for a prompt
And I run :prompt-accept yes
And I wait until the SSL resource page finished loading
Then the javascript message "Script loaded" should be logged
And the page should contain the plaintext "Script loaded"
Scenario: SSL resource error with content.tls.certificate_errors = ask -> no
When I clear SSL errors
And I set content.tls.certificate_errors to ask
And I load an SSL resource page
And I wait for a prompt
And I run :prompt-accept no
And I wait until the SSL resource page finished loading
Then the javascript message "Script loaded" should not be logged
And the page should contain the plaintext "Script not loaded"
Scenario: SSL resource error with content.tls.certificate_errors = ask-block-thirdparty
When I clear SSL errors
And I set content.tls.certificate_errors to ask-block-thirdparty
And I load an SSL resource page
And I wait until the SSL resource page finished loading
Then "Certificate error in resource load: *" should be logged
And the javascript message "Script loaded" should not be logged
And the page should contain the plaintext "Script not loaded"
# Geolocation
Scenario: Always rejecting geolocation
@ -484,7 +522,7 @@ Feature: Prompts
Scenario: Interrupting SSL prompt during a notification prompt
Given I have a fresh instance
When I set content.notifications to ask
And I set content.ssl_strict to ask
And I set content.tls.certificate_errors to ask
And I open data/prompt/notifications.html in a new tab
And I run :click-element id button
And I wait for a prompt

View File

@ -215,7 +215,7 @@ Feature: Special qute:// pages
Scenario: Running :pyeval --file using a non existing file
When I run :debug-pyeval --file nonexistentfile
Then the error "[Errno 2] No such file or directory: 'nonexistentfile'" should be shown
Then the error "[Errno 2] *: 'nonexistentfile'" should be shown
Scenario: Running :pyeval with --quiet
When I run :debug-pyeval --quiet 1+1

View File

@ -8,7 +8,7 @@ Feature: :spawn
Scenario: Running :spawn with command that does not exist
When I run :spawn command_does_not_exist127623
Then the error "Error while spawning command: *" should be shown
Then the error "Command 'command_does_not_exist127623' failed to start: *" should be shown
Scenario: Starting a userscript which doesn't exist
When I run :spawn -u this_does_not_exist

View File

@ -27,7 +27,7 @@ bdd.scenarios('open.feature')
@pytest.mark.parametrize('scheme', ['http://', ''])
def test_open_s(request, quteproc, ssl_server, scheme):
"""Test :open with -s."""
quteproc.set_setting('content.ssl_strict', 'false')
quteproc.set_setting('content.tls.certificate_errors', 'load-insecurely')
quteproc.send_cmd(':open -s {}localhost:{}/'
.format(scheme, ssl_server.port))
if scheme == 'http://' or not request.config.webengine:

View File

@ -36,6 +36,17 @@ def wait_ssl_page_finished_loading(quteproc, ssl_server):
load_status='warn')
@bdd.when("I load an SSL resource page")
def load_ssl_resource_page(quteproc, server, ssl_server):
# We don't wait here as we can get an SSL question.
quteproc.open_path(f'https-script/{ssl_server.port}', port=server.port, wait=False)
@bdd.when("I wait until the SSL resource page finished loading")
def wait_ssl_resource_page_finished_loading(quteproc, server, ssl_server):
quteproc.wait_for_load_finished(f'https-script/{ssl_server.port}', port=server.port)
@bdd.when("I wait for a prompt")
def wait_for_prompt(quteproc):
quteproc.wait_for(message='Asking question *')
@ -66,7 +77,7 @@ def ssl_error_page(request, quteproc):
def test_certificate_error_load_status(request, quteproc, ssl_server):
"""If we load the same page twice, we should get a 'warn' status twice."""
quteproc.set_setting('content.ssl_strict', 'false')
quteproc.set_setting('content.tls.certificate_errors', 'load-insecurely')
for i in range(2):
quteproc.open_path('/', port=ssl_server.port, https=True, wait=False,

View File

@ -29,6 +29,7 @@ import logging
import tempfile
import contextlib
import itertools
import collections
import json
import yaml
@ -453,6 +454,7 @@ class QuteProc(testprocess.Process):
self.basedir = None
self._instance_id = next(instance_counter)
self._run_counter = itertools.count()
self._screenshot_counters = collections.defaultdict(itertools.count)
def _process_line(self, log_line):
"""Check if the line matches any initial lines we're interested in."""
@ -645,8 +647,6 @@ class QuteProc(testprocess.Process):
('auto_save.interval', '0'),
('new_instance_open_target_window', 'last-opened')
]
if not self.request.config.webengine:
settings.append(('content.ssl_strict', 'false'))
for opt, value in settings:
self.set_setting(opt, value)
@ -902,9 +902,14 @@ class QuteProc(testprocess.Process):
"""
for _ in range(5):
tmp_path = self.request.getfixturevalue('tmp_path')
path = tmp_path / 'screenshot.png'
self.send_cmd(f':screenshot --force {path}')
self.wait_for(message=f'Screenshot saved to {path}')
counter = self._screenshot_counters[self.request.node.nodeid]
path = tmp_path / f'screenshot-{next(counter)}.png'
self.send_cmd(f':screenshot {path}')
screenshot_msg = f'Screenshot saved to {path}'
self.wait_for(message=screenshot_msg)
print(screenshot_msg)
img = QImage(str(path))
assert not img.isNull()
@ -919,8 +924,9 @@ class QuteProc(testprocess.Process):
# Rendering might not be completed yet...
time.sleep(0.5)
raise ValueError(
f"Pixel probing for {probe_color} failed (got {probed_color} on last try)")
# Using assert again for pytest introspection
assert probed_color == probe_color, "Color probing failed, values on last try:"
raise utils.Unreachable()
def press_keys(self, keys):
"""Press the given keys using :fake-key."""

View File

@ -62,7 +62,11 @@ class Request(testprocess.Line):
def _check_status(self):
"""Check if the http status is what we expected."""
path_to_statuses = {
'/favicon.ico': [HTTPStatus.OK, HTTPStatus.PARTIAL_CONTENT],
'/favicon.ico': [
HTTPStatus.OK,
HTTPStatus.PARTIAL_CONTENT,
HTTPStatus.NOT_MODIFIED,
],
'/does-not-exist': [HTTPStatus.NOT_FOUND],
'/does-not-exist-2': [HTTPStatus.NOT_FOUND],

Some files were not shown because too many files have changed in this diff Show More