Merge branch 'master' into master
This commit is contained in:
commit
b41d7ba203
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@ -0,0 +1 @@
|
||||
doc/changelog.asciidoc merge=union
|
5
.github/CONTRIBUTING.asciidoc
vendored
5
.github/CONTRIBUTING.asciidoc
vendored
@ -1,8 +1,3 @@
|
||||
IMPORTANT: I'm currently (July 2018) more busy than usual until September,
|
||||
because of exams coming up. Review of non-trivial pull requests will thus be
|
||||
delayed until then. If you're reading this note after mid-September, please
|
||||
open an issue.
|
||||
|
||||
- Before you start to work on something, please leave a comment on the relevant
|
||||
issue (or open one). This makes sure there is no duplicate work done.
|
||||
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,7 +16,6 @@ __pycache__
|
||||
/doc/*.html
|
||||
/README.html
|
||||
/qutebrowser/html/doc/
|
||||
/qutebrowser/html/*.html
|
||||
/.venv*
|
||||
/.coverage
|
||||
/htmlcov
|
||||
|
@ -27,10 +27,24 @@ Added
|
||||
- New `content.mouse_lock` setting to handle HTML5 pointer locking.
|
||||
- New `completion.web_history.exclude` setting which hides a list of URL
|
||||
patterns from the completion.
|
||||
- Rewritten PDF.js support:
|
||||
* PDF.js support and the `content.pdfjs` setting are now available with
|
||||
QtWebEngine.
|
||||
* Opening a PDF file now doesn't start a second request anymore.
|
||||
* Opening PDFs on https:// sites now works properly.
|
||||
- New `qt.process_model` setting which can be used to change Chromium's process
|
||||
model.
|
||||
- New `qt.low_end_device_mode` setting which turns on Chromium's low-end device
|
||||
mode. This mode uses less RAM, but the expense of performance.
|
||||
- New `content.webrtc_ip_handling_policy` setting, which allows more
|
||||
fine-grained/restrictive control about which IPs are exposed via WebRTC.
|
||||
- Running qutebrowser with QtWebKit or Qt < 5.9 now shows a warning (only
|
||||
once), as support for those is going to be removed in a future release.
|
||||
|
||||
Changed
|
||||
~~~~~~~
|
||||
|
||||
- The `content.headers.referer` setting now works on QtWebEngine.
|
||||
- The `:repeat` command now takes a count which is multiplied with the given
|
||||
"times" argument.
|
||||
- The default keybinding to leave passthrough mode was changed from `<Ctrl-V>`
|
||||
@ -43,6 +57,29 @@ Changed
|
||||
already did).
|
||||
- The `completion.web_history_max_items` setting got renamed to
|
||||
`completion.web_history.max_items`.
|
||||
- The Makefile shipped with qutebrowser now supports overriding variables
|
||||
DATADIR and MANDIR.
|
||||
- Various performance improvements when many tabs are opened.
|
||||
- Regenerating completion history now shows a progress dialog.
|
||||
- Make qute:// pages work properly on Qt 5.11.2
|
||||
- The `content.autoplay` setting now supports URL patterns on Qt >= 5.11.
|
||||
- The `content.host_blocking.whitelist` setting now takes a list of URL
|
||||
patterns instead of globs.
|
||||
|
||||
Fixed
|
||||
~~~~~
|
||||
|
||||
- Error when passing a substring with spaces to `:tab-take`.
|
||||
- Greasemonkey scripts which start with an UTF-8 BOM are now handled correctly.
|
||||
- When no documentation has been generated, the plaintext documentation now can
|
||||
be shown for more files such as `qute://help/userscripts.html`.
|
||||
|
||||
Removed
|
||||
~~~~~~~
|
||||
|
||||
- Support for importing pre-v1.0.0 history files has been removed.
|
||||
- The `content.webrtc_public_interfaces_only` setting has been removed and
|
||||
replaced by `content.webrtc_ip_handling_policy`.
|
||||
|
||||
v1.4.2
|
||||
------
|
||||
@ -1615,7 +1652,7 @@ Changed
|
||||
`tabs.bg/fg.selected.odd/even`.
|
||||
- `:spawn --userscript` and `:hint` with the `userscript` target now look up
|
||||
relative paths in `~/.local/share/qutebrowser/userscripts` or
|
||||
`$XDG_DATA_DIR`. Using a binary in `$PATH` won't work anymore with
|
||||
`$XDG_DATA_HOME`. Using a binary in `$PATH` won't work anymore with
|
||||
`--userscript`.
|
||||
- New design for error pages
|
||||
- Link filtering for hints now checks if the text is contained anywhere in
|
||||
|
@ -5,11 +5,6 @@ The Compiler <mail@qutebrowser.org>
|
||||
:data-uri:
|
||||
:toc:
|
||||
|
||||
IMPORTANT: I'm currently (July 2018) more busy than usual until September,
|
||||
because of exams coming up. Review of non-trivial pull requests will thus be
|
||||
delayed until then. If you're reading this note after mid-September, please
|
||||
open an issue.
|
||||
|
||||
I `<3` footnote:[Of course, that says `<3` in HTML.] contributors!
|
||||
|
||||
This document contains guidelines for contributing to qutebrowser, as well as
|
||||
|
@ -576,7 +576,7 @@ Start hinting.
|
||||
- With `userscript`: The userscript to execute. Either store
|
||||
the userscript in
|
||||
`~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`), or use an absolute
|
||||
(or `$XDG_DATA_HOME`), or use an absolute
|
||||
path.
|
||||
- With `fill`: The command to fill the statusbar with.
|
||||
`{hint-url}` will get replaced by the selected
|
||||
@ -1193,7 +1193,7 @@ Spawn a command in a shell.
|
||||
* +*-u*+, +*--userscript*+: Run the command as a userscript. You can use an absolute path, or store the userscript in one of those
|
||||
locations:
|
||||
- `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`)
|
||||
(or `$XDG_DATA_HOME`)
|
||||
- `/usr/share/qutebrowser/userscripts`
|
||||
|
||||
* +*-v*+, +*--verbose*+: Show notifications when the command started/exited.
|
||||
@ -1342,6 +1342,9 @@ Take a tab from another window.
|
||||
* +'index'+: The [win_id/]index of the tab to take. Or a substring in which case the closest match will be taken.
|
||||
|
||||
|
||||
==== note
|
||||
* This command does not split arguments after the last argument and handles quotes literally.
|
||||
|
||||
[[unbind]]
|
||||
=== unbind
|
||||
Syntax: +:unbind [*--mode* 'mode'] 'key'+
|
||||
|
@ -129,7 +129,7 @@
|
||||
|<<content.headers.user_agent,content.headers.user_agent>>|User agent to send. Unset to send the default.
|
||||
|<<content.host_blocking.enabled,content.host_blocking.enabled>>|Enable host blocking.
|
||||
|<<content.host_blocking.lists,content.host_blocking.lists>>|List of URLs of lists which contain hosts to block.
|
||||
|<<content.host_blocking.whitelist,content.host_blocking.whitelist>>|List of domains that should always be loaded, despite being ad-blocked.
|
||||
|<<content.host_blocking.whitelist,content.host_blocking.whitelist>>|A list of patterns that should always be loaded, despite being ad-blocked.
|
||||
|<<content.hyperlink_auditing,content.hyperlink_auditing>>|Enable hyperlink auditing (`<a ping>`).
|
||||
|<<content.images,content.images>>|Load images automatically in web pages.
|
||||
|<<content.javascript.alert,content.javascript.alert>>|Show javascript alerts.
|
||||
@ -158,7 +158,7 @@
|
||||
|<<content.ssl_strict,content.ssl_strict>>|Validate SSL handshakes.
|
||||
|<<content.user_stylesheets,content.user_stylesheets>>|List of user stylesheet filenames to use.
|
||||
|<<content.webgl,content.webgl>>|Enable WebGL.
|
||||
|<<content.webrtc_public_interfaces_only,content.webrtc_public_interfaces_only>>|Only expose public interfaces via WebRTC.
|
||||
|<<content.webrtc_ip_handling_policy,content.webrtc_ip_handling_policy>>|Which interfaces to expose via WebRTC.
|
||||
|<<content.windowed_fullscreen,content.windowed_fullscreen>>|Limit fullscreen to the browser window (does not expand to fill the screen).
|
||||
|<<content.xss_auditing,content.xss_auditing>>|Monitor load requests for cross-site scripting attempts.
|
||||
|<<downloads.location.directory,downloads.location.directory>>|Directory to save downloads to.
|
||||
@ -229,6 +229,8 @@
|
||||
|<<qt.force_platform,qt.force_platform>>|Force a Qt platform to use.
|
||||
|<<qt.force_software_rendering,qt.force_software_rendering>>|Force software rendering for QtWebEngine.
|
||||
|<<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.
|
||||
|<<scrolling.bar,scrolling.bar>>|Show a scrollbar.
|
||||
|<<scrolling.smooth,scrolling.smooth>>|Enable smooth scrolling for web pages.
|
||||
|<<search.ignore_case,search.ignore_case>>|When to find text on a page case-insensitively.
|
||||
@ -1488,7 +1490,9 @@ Default:
|
||||
[[content.autoplay]]
|
||||
=== content.autoplay
|
||||
Automatically start playing `<video>` elements.
|
||||
Note this option needs a restart with QtWebEngine on Qt < 5.11.
|
||||
Note: On Qt < 5.11, this option needs a restart and does not support URL patterns.
|
||||
|
||||
This setting supports URL patterns.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
|
||||
@ -1673,6 +1677,8 @@ Default: +pass:[true]+
|
||||
=== content.headers.referer
|
||||
When to send the Referer header.
|
||||
The Referer header tells websites from which website you were coming from when visiting them.
|
||||
No restart is needed with QtWebKit.
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,String>>
|
||||
|
||||
@ -1680,12 +1686,10 @@ Valid values:
|
||||
|
||||
* +always+: Always send the Referer.
|
||||
* +never+: Never send the Referer. This is not recommended, as some sites may break.
|
||||
* +same-domain+: Only send the Referer for the same domain. This will still protect your privacy, but shouldn't break any sites.
|
||||
* +same-domain+: Only send the Referer for the same domain. This will still protect your privacy, but shouldn't break any sites. With QtWebEngine, the referer will still be sent for other domains, but with stripped path information.
|
||||
|
||||
Default: +pass:[same-domain]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
[[content.headers.user_agent]]
|
||||
=== content.headers.user_agent
|
||||
User agent to send. Unset to send the default.
|
||||
@ -1725,11 +1729,10 @@ Default:
|
||||
|
||||
[[content.host_blocking.whitelist]]
|
||||
=== content.host_blocking.whitelist
|
||||
List of domains that should always be loaded, despite being ad-blocked.
|
||||
Domains may contain * and ? wildcards and are otherwise required to exactly match the requested domain.
|
||||
A list of patterns that should always be loaded, despite being ad-blocked.
|
||||
Local domains are always exempt from hostblocking.
|
||||
|
||||
Type: <<types,List of String>>
|
||||
Type: <<types,List of UrlPattern>>
|
||||
|
||||
Default:
|
||||
|
||||
@ -1941,8 +1944,6 @@ Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
This setting is only available with the QtWebKit backend.
|
||||
|
||||
[[content.persistent_storage]]
|
||||
=== content.persistent_storage
|
||||
Allow websites to request persistent storage quota via `navigator.webkitPersistentStorage.requestQuota`.
|
||||
@ -2071,14 +2072,22 @@ Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[true]+
|
||||
|
||||
[[content.webrtc_public_interfaces_only]]
|
||||
=== content.webrtc_public_interfaces_only
|
||||
Only expose public interfaces via WebRTC.
|
||||
On Qt 5.9, this option requires a restart. On Qt 5.10, this option doesn't work at all because of a Qt bug. On Qt >= 5.11, no restart is required.
|
||||
[[content.webrtc_ip_handling_policy]]
|
||||
=== content.webrtc_ip_handling_policy
|
||||
Which interfaces to expose via WebRTC.
|
||||
On Qt 5.10, this option doesn't work because of a Qt bug.
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,Bool>>
|
||||
Type: <<types,String>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
Valid values:
|
||||
|
||||
* +all-interfaces+: WebRTC has the right to enumerate all interfaces and bind them to discover public interfaces.
|
||||
* +default-public-and-private-interfaces+: WebRTC should only use the default route used by http. This also exposes the associated default private address. Default route is the route chosen by the OS on a multi-homed endpoint.
|
||||
* +default-public-interface-only+: WebRTC should only use the default route used by http. This doesn't expose any local addresses.
|
||||
* +disable-non-proxied-udp+: WebRTC should only use TCP to contact peers or servers unless the proxy server supports UDP. This doesn't expose any local addresses either.
|
||||
|
||||
Default: +pass:[all-interfaces]+
|
||||
|
||||
On QtWebEngine, this setting requires Qt 5.9.2 or newer.
|
||||
|
||||
@ -2757,6 +2766,46 @@ Type: <<types,Bool>>
|
||||
|
||||
Default: +pass:[false]+
|
||||
|
||||
[[qt.low_end_device_mode]]
|
||||
=== qt.low_end_device_mode
|
||||
When to use Chromium's low-end device mode.
|
||||
This improves the RAM usage of renderer processes, at the expense of performance.
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,String>>
|
||||
|
||||
Valid values:
|
||||
|
||||
* +always+: Always use low-end device mode.
|
||||
* +auto+: Decide automatically (uses low-end mode with < 1 GB available RAM).
|
||||
* +never+: Never use low-end device mode.
|
||||
|
||||
Default: +pass:[auto]+
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
[[qt.process_model]]
|
||||
=== qt.process_model
|
||||
Which Chromium process model to use.
|
||||
Alternative process models use less resources, but decrease security and robustness.
|
||||
See the following pages for more details:
|
||||
|
||||
- https://www.chromium.org/developers/design-documents/process-models
|
||||
- https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
|
||||
This setting requires a restart.
|
||||
|
||||
Type: <<types,String>>
|
||||
|
||||
Valid values:
|
||||
|
||||
* +process-per-site-instance+: Pages from separate sites are put into separate processes and separate visits to the same site are also isolated.
|
||||
* +process-per-site+: Pages from separate sites are put into separate processes. Unlike Process per Site Instance, all visits to the same site will share an OS process. The benefit of this model is reduced memory consumption, because more web pages will share processes. The drawbacks include reduced security, robustness, and responsiveness.
|
||||
* +single-process+: Run all tabs in a single process. This should be used for debugging purposes only, and it disables `:open --private`.
|
||||
|
||||
Default: +pass:[process-per-site-instance]+
|
||||
|
||||
This setting is only available with the QtWebEngine backend.
|
||||
|
||||
[[scrolling.bar]]
|
||||
=== scrolling.bar
|
||||
Show a scrollbar.
|
||||
|
@ -1,7 +1,9 @@
|
||||
PYTHON = python3
|
||||
PREFIX = /usr/local
|
||||
DESTDIR =
|
||||
PREFIX ?= /usr/local
|
||||
ICONSIZES = 16 24 32 48 64 128 256 512
|
||||
DATAROOTDIR = $(PREFIX)/share
|
||||
DATADIR ?= $(DATAROOTDIR)
|
||||
MANDIR ?= $(DATAROOTDIR)/man
|
||||
|
||||
SETUPTOOLSOPTIONS =
|
||||
ifdef DESTDIR
|
||||
@ -16,18 +18,18 @@ doc/qutebrowser.1.html:
|
||||
install: doc/qutebrowser.1.html
|
||||
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
|
||||
install -Dm644 misc/qutebrowser.appdata.xml \
|
||||
"$(DESTDIR)$(PREFIX)/share/metainfo/qutebrowser.appdata.xml"
|
||||
"$(DESTDIR)$(DATADIR)/metainfo/qutebrowser.appdata.xml"
|
||||
install -Dm644 doc/qutebrowser.1 \
|
||||
"$(DESTDIR)$(PREFIX)/share/man/man1/qutebrowser.1"
|
||||
"$(DESTDIR)$(MANDIR)/man1/qutebrowser.1"
|
||||
install -Dm644 misc/qutebrowser.desktop \
|
||||
"$(DESTDIR)$(PREFIX)/share/applications/qutebrowser.desktop"
|
||||
"$(DESTDIR)$(DATADIR)/applications/qutebrowser.desktop"
|
||||
$(foreach i,$(ICONSIZES),install -Dm644 "icons/qutebrowser-$(i)x$(i).png" \
|
||||
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
|
||||
"$(DESTDIR)$(DATADIR)/icons/hicolor/$(i)x$(i)/apps/qutebrowser.png";)
|
||||
install -Dm644 icons/qutebrowser.svg \
|
||||
"$(DESTDIR)$(PREFIX)/share/icons/hicolor/scalable/apps/qutebrowser.svg"
|
||||
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/userscripts/" \
|
||||
"$(DESTDIR)$(DATADIR)/icons/hicolor/scalable/apps/qutebrowser.svg"
|
||||
install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/userscripts/" \
|
||||
$(wildcard misc/userscripts/*)
|
||||
install -Dm755 -t "$(DESTDIR)$(PREFIX)/share/qutebrowser/scripts/" \
|
||||
install -Dm755 -t "$(DESTDIR)$(DATADIR)/qutebrowser/scripts/" \
|
||||
$(filter-out scripts/__init__.py scripts/__pycache__ scripts/dev \
|
||||
scripts/testbrowser scripts/asciidoc2html.py scripts/setupcommon.py \
|
||||
scripts/link_pyqt.py,$(wildcard scripts/*))
|
||||
|
@ -3,6 +3,6 @@
|
||||
appdirs==1.4.3
|
||||
packaging==17.1
|
||||
pyparsing==2.2.0
|
||||
setuptools==40.2.0
|
||||
setuptools==40.3.0
|
||||
six==1.11.0
|
||||
wheel==0.31.1
|
||||
|
@ -4,4 +4,4 @@ altgraph==0.16.1
|
||||
future==0.16.0
|
||||
macholib==1.11
|
||||
pefile==2018.8.8
|
||||
PyInstaller==3.3.1
|
||||
PyInstaller==3.4
|
||||
|
@ -1,23 +0,0 @@
|
||||
# This file is automatically generated by scripts/dev/recompile_requirements.py
|
||||
|
||||
asn1crypto==0.24.0
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
certifi==2018.8.24
|
||||
cffi==1.11.5
|
||||
chardet==3.0.4
|
||||
cryptography==2.3.1
|
||||
github3.py==1.2.0
|
||||
idna==2.7
|
||||
isort==4.3.4
|
||||
jwcrypto==0.5.0
|
||||
lazy-object-proxy==1.3.1
|
||||
mccabe==0.6.1
|
||||
pycparser==2.18
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
python-dateutil==2.7.3
|
||||
./scripts/dev/pylint_checkers
|
||||
requests==2.19.1
|
||||
six==1.11.0
|
||||
uritemplate==3.0.0
|
||||
urllib3==1.23
|
||||
wrapt==1.10.11
|
@ -1,11 +0,0 @@
|
||||
-e git+https://github.com/PyCQA/astroid.git#egg=astroid
|
||||
-e git+https://github.com/PyCQA/pylint.git#egg=pylint
|
||||
./scripts/dev/pylint_checkers
|
||||
requests
|
||||
github3.py
|
||||
|
||||
# remove @commit-id for scm installs
|
||||
#@ replace: @.*# #
|
||||
|
||||
# fix qute-pylint location
|
||||
#@ replace: qute-pylint==.* ./scripts/dev/pylint_checkers
|
@ -13,7 +13,7 @@ fields==5.0.0
|
||||
Flask==1.0.2
|
||||
glob2==0.6
|
||||
hunter==2.0.2
|
||||
hypothesis==3.70.3
|
||||
hypothesis==3.71.8
|
||||
itsdangerous==0.24
|
||||
# Jinja2==2.10
|
||||
Mako==1.0.7
|
||||
@ -24,10 +24,10 @@ parse-type==0.4.2
|
||||
pluggy==0.7.1
|
||||
py==1.6.0
|
||||
py-cpuinfo==4.0.0
|
||||
pytest==3.6.4 # rq.filter: != 3.7, != 3.7.1, != 3.7.2, != 3.7.3, != 3.7.4
|
||||
pytest==3.6.4 # rq.filter: <3.7
|
||||
pytest-bdd==2.21.0
|
||||
pytest-benchmark==3.1.1
|
||||
pytest-cov==2.5.1
|
||||
pytest-cov==2.6.0
|
||||
pytest-faulthandler==1.5.0
|
||||
pytest-instafail==0.4.0
|
||||
pytest-mock==1.10.0
|
||||
|
@ -19,4 +19,4 @@ pytest-xvfb
|
||||
vulture
|
||||
|
||||
#@ ignore: Jinja2, MarkupSafe, colorama
|
||||
#@ filter: pytest != 3.7, != 3.7.1, != 3.7.2, != 3.7.3, != 3.7.4
|
||||
#@ filter: pytest <3.7
|
||||
|
@ -3,5 +3,6 @@
|
||||
pluggy==0.7.1
|
||||
py==1.6.0
|
||||
six==1.11.0
|
||||
tox==3.2.1
|
||||
toml==0.9.6
|
||||
tox==3.3.0
|
||||
virtualenv==16.0.0
|
||||
|
61
misc/userscripts/README.md
Normal file
61
misc/userscripts/README.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Userscripts
|
||||
|
||||
The following userscripts are included in the current directory.
|
||||
|
||||
- [cast](./cast): Cast content on your Chromecast using [castnow][]. Only
|
||||
[youtube-dl][] downloadable content.
|
||||
- [dmenu_qutebrowser](./dmenu_qutebrowser): Pipes history, quickmarks, and URL into dmenu.
|
||||
- [format_json](./format_json): Pretty prints current page's JSON code in other
|
||||
tab.
|
||||
- [getbib](./getbib): Scraping the current web page for DOIs and downloading
|
||||
corresponding bibtex information.
|
||||
- [open_download](./open_download): Opens a rofi menu with
|
||||
all files from the download directory and opens the selected file.
|
||||
- [openfeeds](./openfeeds): Opens all links to feeds defined in the head of a site.
|
||||
- [password_fill](./password_fill): Find a username/password entry and fill it
|
||||
with credentials given by the configured backend (currently only pass) for the
|
||||
current website.
|
||||
- [qute-keepass](./qute-keepass): Insertion of usernames and passwords from keepass
|
||||
databases using pykeepass.
|
||||
- [qute-pass](./qute-pass): Insert login information using pass and a
|
||||
dmenu-compatible application (e.g. dmenu, rofi -dmenu, ...).
|
||||
- [qutedmenu](./qutedmenu): Handle open -s && open -t with bemenu.
|
||||
- [readability](./readability): Executes python-readability on current page and
|
||||
opens the summary as new tab.
|
||||
- [ripbang](./ripbang): Adds DuckDuckGo bang as searchengine.
|
||||
- [rss](./rss): Keeps track of URLs in RSS feeds and opens new ones.
|
||||
- [taskadd](./taskadd): Adds a task to taskwarrior.
|
||||
- [tor_identity](./tor_identity): Change your tor identity.
|
||||
- [view_in_mpv](./view_in_mpv): Views the current web page in mpv using
|
||||
sensible mpv-flags.
|
||||
|
||||
[castnow]: https://github.com/xat/castnow
|
||||
[youtube-dl]: https://rg3.github.io/youtube-dl/
|
||||
|
||||
## Others
|
||||
|
||||
The following userscripts can be found on their own repositories.
|
||||
|
||||
- [qurlshare](https://github.com/sim590/qurlshare): *secure* sharing of an URL between qutebrowser
|
||||
instances using a distributed hash table.
|
||||
- [qutebrowser-userscripts](https://github.com/cryzed/qutebrowser-userscripts):
|
||||
a small pack of userscripts.
|
||||
- [qutebrowser-zotero](https://github.com/parchd-1/qutebrowser-zotero): connects
|
||||
qutebrowser to [Zotero][] standalone.
|
||||
- [qute.match](https://github.com/bziur/qute.match): execute script based on
|
||||
visisted url.
|
||||
- [qutepocket](https://github.com/kepi/qutepocket): Add URL to your [Pocket][]
|
||||
bookmark manager.
|
||||
- [qb-scripts](https://github.com/peterjschroeder/qb-scripts): a small pack of
|
||||
userscripts.
|
||||
- [instapaper.zsh](https://github.com/dmcgrady/instapaper.zsh): Add URL to
|
||||
your [Instapaper][] bookmark manager.
|
||||
- [qtb.us](https://github.com/Chinggis6/qtb.us): small pack of userscripts.
|
||||
- [pinboard.zsh](https://github.com/dmix/pinboard.zsh): Add URL to your
|
||||
[Pinboard][] bookmark manager.
|
||||
|
||||
[Zotero]: https://www.zotero.org/
|
||||
[Pocket]: https://getpocket.com/
|
||||
[Instapaper]: https://www.instapaper.com/
|
||||
[Pinboard]: https://pinboard.in/
|
||||
|
@ -169,7 +169,7 @@ def main(arguments):
|
||||
|
||||
# Match username
|
||||
target = selection if arguments.username_target == 'path' else secret
|
||||
match = re.match(arguments.username_pattern, target)
|
||||
match = re.search(arguments.username_pattern, target, re.MULTILINE)
|
||||
if not match:
|
||||
stderr('Failed to match username pattern on {}!'.format(arguments.username_target))
|
||||
return ExitCodes.COULD_NOT_MATCH_USERNAME
|
||||
|
@ -72,9 +72,9 @@ from qutebrowser.keyinput import macros
|
||||
from qutebrowser.mainwindow import mainwindow, prompt
|
||||
from qutebrowser.misc import (readline, ipc, savemanager, sessions,
|
||||
crashsignal, earlyinit, sql, cmdhistory,
|
||||
backendproblem)
|
||||
backendproblem, objects)
|
||||
from qutebrowser.utils import (log, version, message, utils, urlutils, objreg,
|
||||
usertypes, standarddir, error)
|
||||
usertypes, standarddir, error, qtutils)
|
||||
# pylint: disable=unused-import
|
||||
# We import those to run the cmdutils.register decorators.
|
||||
from qutebrowser.mainwindow.statusbar import command
|
||||
@ -185,8 +185,6 @@ def init(args, crash_handler):
|
||||
QDesktopServices.setUrlHandler('https', open_desktopservices_url)
|
||||
QDesktopServices.setUrlHandler('qute', open_desktopservices_url)
|
||||
|
||||
objreg.get('web-history').import_txt()
|
||||
|
||||
log.init.debug("Init done!")
|
||||
crash_handler.raise_crashdlg()
|
||||
|
||||
@ -353,10 +351,6 @@ def _open_startpage(win_id=None):
|
||||
def _open_special_pages(args):
|
||||
"""Open special notification pages which are only shown once.
|
||||
|
||||
Currently this is:
|
||||
- Quickstart page if it's the first start.
|
||||
- Legacy QtWebKit warning if needed.
|
||||
|
||||
Args:
|
||||
args: The argparse namespace.
|
||||
"""
|
||||
@ -368,25 +362,30 @@ def _open_special_pages(args):
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
|
||||
# Quickstart page
|
||||
pages = [
|
||||
# state, condition, URL
|
||||
('quickstart-done',
|
||||
True,
|
||||
'https://www.qutebrowser.org/quickstart.html'),
|
||||
|
||||
quickstart_done = general_sect.get('quickstart-done') == '1'
|
||||
('config-migration-shown',
|
||||
os.path.exists(os.path.join(standarddir.config(),
|
||||
'qutebrowser.conf')),
|
||||
'qute://help/configuring.html'),
|
||||
|
||||
if not quickstart_done:
|
||||
tabbed_browser.tabopen(
|
||||
QUrl('https://www.qutebrowser.org/quickstart.html'))
|
||||
general_sect['quickstart-done'] = '1'
|
||||
('webkit-warning-shown',
|
||||
objects.backend == usertypes.Backend.QtWebKit,
|
||||
'qute://warning/webkit'),
|
||||
|
||||
# Setting migration page
|
||||
('old-qt-warning-shown',
|
||||
not qtutils.version_check('5.9'),
|
||||
'qute://warning/old-qt'),
|
||||
]
|
||||
|
||||
needs_migration = os.path.exists(
|
||||
os.path.join(standarddir.config(), 'qutebrowser.conf'))
|
||||
migration_shown = general_sect.get('config-migration-shown') == '1'
|
||||
|
||||
if needs_migration and not migration_shown:
|
||||
tabbed_browser.tabopen(QUrl('qute://help/configuring.html'),
|
||||
background=False)
|
||||
general_sect['config-migration-shown'] = '1'
|
||||
for state, condition, url in pages:
|
||||
if general_sect.get(state) != '1' and condition:
|
||||
tabbed_browser.tabopen(QUrl(url), background=False)
|
||||
general_sect[state] = '1'
|
||||
|
||||
|
||||
def on_focus_changed(_old, new):
|
||||
|
@ -24,7 +24,6 @@ import os.path
|
||||
import functools
|
||||
import posixpath
|
||||
import zipfile
|
||||
import fnmatch
|
||||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.config import config
|
||||
@ -32,7 +31,7 @@ from qutebrowser.utils import objreg, standarddir, log, message
|
||||
from qutebrowser.commands import cmdutils
|
||||
|
||||
|
||||
def guess_zip_filename(zf):
|
||||
def _guess_zip_filename(zf):
|
||||
"""Guess which file to use inside a zip file.
|
||||
|
||||
Args:
|
||||
@ -54,26 +53,26 @@ def get_fileobj(byte_io):
|
||||
if zipfile.is_zipfile(byte_io):
|
||||
byte_io.seek(0) # rewind what zipfile.is_zipfile did
|
||||
zf = zipfile.ZipFile(byte_io)
|
||||
filename = guess_zip_filename(zf)
|
||||
filename = _guess_zip_filename(zf)
|
||||
byte_io = zf.open(filename, mode='r')
|
||||
else:
|
||||
byte_io.seek(0) # rewind what zipfile.is_zipfile did
|
||||
return byte_io
|
||||
|
||||
|
||||
def is_whitelisted_host(host):
|
||||
"""Check if the given host is on the adblock whitelist.
|
||||
def _is_whitelisted_url(url):
|
||||
"""Check if the given URL is on the adblock whitelist.
|
||||
|
||||
Args:
|
||||
host: The host of the request as string.
|
||||
url: The URL to check as QUrl.
|
||||
"""
|
||||
for pattern in config.val.content.host_blocking.whitelist:
|
||||
if fnmatch.fnmatch(host, pattern.lower()):
|
||||
if pattern.matches(url):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class FakeDownload:
|
||||
class _FakeDownload:
|
||||
|
||||
"""A download stub to use on_download_finished with local files."""
|
||||
|
||||
@ -118,7 +117,7 @@ class HostBlocker:
|
||||
host = url.host()
|
||||
return ((host in self._blocked_hosts or
|
||||
host in self._config_blocked_hosts) and
|
||||
not is_whitelisted_host(host))
|
||||
not _is_whitelisted_url(url))
|
||||
|
||||
def _read_hosts_file(self, filename, target):
|
||||
"""Read hosts from the given filename.
|
||||
@ -189,7 +188,7 @@ class HostBlocker:
|
||||
auto_remove=True)
|
||||
self._in_progress.append(download)
|
||||
download.finished.connect(
|
||||
functools.partial(self.on_download_finished, download))
|
||||
functools.partial(self._on_download_finished, download))
|
||||
|
||||
def _import_local(self, filename):
|
||||
"""Adds the contents of a file to the blocklist.
|
||||
@ -285,7 +284,7 @@ class HostBlocker:
|
||||
message.error("adblock: {} read errors for {}".format(
|
||||
error_count, byte_io.name))
|
||||
|
||||
def on_lists_downloaded(self):
|
||||
def _on_lists_downloaded(self):
|
||||
"""Install block lists after files have been downloaded."""
|
||||
with open(self._local_hosts_file, 'w', encoding='utf-8') as f:
|
||||
for host in sorted(self._blocked_hosts):
|
||||
@ -304,7 +303,7 @@ class HostBlocker:
|
||||
except OSError as e:
|
||||
log.misc.exception("Failed to delete hosts file: {}".format(e))
|
||||
|
||||
def on_download_finished(self, download):
|
||||
def _on_download_finished(self, download):
|
||||
"""Check if all downloads are finished and if so, trigger reading.
|
||||
|
||||
Arguments:
|
||||
@ -319,6 +318,6 @@ class HostBlocker:
|
||||
download.fileobj.close()
|
||||
if not self._in_progress:
|
||||
try:
|
||||
self.on_lists_downloaded()
|
||||
self._on_lists_downloaded()
|
||||
except OSError:
|
||||
log.misc.exception("Failed to write host block list!")
|
||||
|
@ -66,6 +66,11 @@ class CommandDispatcher:
|
||||
|
||||
def _new_tabbed_browser(self, private):
|
||||
"""Get a tabbed-browser from a new window."""
|
||||
args = QApplication.instance().arguments()
|
||||
if private and '--single-process' in args:
|
||||
raise cmdexc.CommandError("Private windows are unavailable with "
|
||||
"the single-process process model.")
|
||||
|
||||
new_window = mainwindow.MainWindow(private=private)
|
||||
new_window.show()
|
||||
return new_window.tabbed_browser
|
||||
@ -485,7 +490,8 @@ class CommandDispatcher:
|
||||
new_tabbed_browser.widget.set_tab_pinned(newtab, curtab.data.pinned)
|
||||
return newtab
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
maxsplit=0)
|
||||
@cmdutils.argument('index', completion=miscmodels.other_buffer)
|
||||
def tab_take(self, index):
|
||||
"""Take a tab from another window.
|
||||
@ -1183,7 +1189,7 @@ class CommandDispatcher:
|
||||
absolute path, or store the userscript in one of those
|
||||
locations:
|
||||
- `~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`)
|
||||
(or `$XDG_DATA_HOME`)
|
||||
- `/usr/share/qutebrowser/userscripts`
|
||||
verbose: Show notifications when the command started/exited.
|
||||
output: Whether the output should be shown in a new tab.
|
||||
|
@ -32,10 +32,11 @@ import enum
|
||||
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
|
||||
QTimer, QAbstractListModel, QUrl)
|
||||
|
||||
from qutebrowser.browser import pdfjs
|
||||
from qutebrowser.commands import cmdexc, cmdutils
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
|
||||
qtutils)
|
||||
qtutils, objreg)
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
@ -224,9 +225,6 @@ class _DownloadTarget:
|
||||
|
||||
"""Abstract base class for different download targets."""
|
||||
|
||||
def __init__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def suggested_filename(self):
|
||||
"""Get the suggested filename for this download target."""
|
||||
raise NotImplementedError
|
||||
@ -243,7 +241,6 @@ class FileDownloadTarget(_DownloadTarget):
|
||||
"""
|
||||
|
||||
def __init__(self, filename, force_overwrite=False):
|
||||
# pylint: disable=super-init-not-called
|
||||
self.filename = filename
|
||||
self.force_overwrite = force_overwrite
|
||||
|
||||
@ -263,7 +260,6 @@ class FileObjDownloadTarget(_DownloadTarget):
|
||||
"""
|
||||
|
||||
def __init__(self, fileobj):
|
||||
# pylint: disable=super-init-not-called
|
||||
self.fileobj = fileobj
|
||||
|
||||
def suggested_filename(self):
|
||||
@ -290,7 +286,6 @@ class OpenFileDownloadTarget(_DownloadTarget):
|
||||
"""
|
||||
|
||||
def __init__(self, cmdline=None):
|
||||
# pylint: disable=super-init-not-called
|
||||
self.cmdline = cmdline
|
||||
|
||||
def suggested_filename(self):
|
||||
@ -300,6 +295,17 @@ class OpenFileDownloadTarget(_DownloadTarget):
|
||||
return 'temporary file'
|
||||
|
||||
|
||||
class PDFJSDownloadTarget(_DownloadTarget):
|
||||
|
||||
"""Open the download via PDF.js."""
|
||||
|
||||
def suggested_filename(self):
|
||||
raise NoFilenameError
|
||||
|
||||
def __str__(self):
|
||||
return 'temporary PDF.js file'
|
||||
|
||||
|
||||
class DownloadItemStats(QObject):
|
||||
|
||||
"""Statistics (bytes done, total bytes, time, etc.) about a download.
|
||||
@ -405,6 +411,8 @@ class AbstractDownloadItem(QObject):
|
||||
arg: The error message as string.
|
||||
remove_requested: Emitted when the removal of this download was
|
||||
requested.
|
||||
pdfjs_requested: Emitted when PDF.js should be opened with the given
|
||||
filename.
|
||||
"""
|
||||
|
||||
data_changed = pyqtSignal()
|
||||
@ -412,6 +420,7 @@ class AbstractDownloadItem(QObject):
|
||||
error = pyqtSignal(str)
|
||||
cancelled = pyqtSignal()
|
||||
remove_requested = pyqtSignal()
|
||||
pdfjs_requested = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
@ -730,6 +739,19 @@ class AbstractDownloadItem(QObject):
|
||||
return
|
||||
self.open_file(cmdline)
|
||||
|
||||
def _pdfjs_if_successful(self):
|
||||
"""Open the file via PDF.js if downloading was successful."""
|
||||
if not self.successful:
|
||||
log.downloads.debug("{} finished but not successful, not opening!"
|
||||
.format(self))
|
||||
return
|
||||
|
||||
filename = self._get_open_filename()
|
||||
if filename is None: # pragma: no cover
|
||||
log.downloads.error("No filename to open the download!")
|
||||
return
|
||||
self.pdfjs_requested.emit(os.path.basename(filename))
|
||||
|
||||
def set_target(self, target):
|
||||
"""Set the target for a given download.
|
||||
|
||||
@ -741,7 +763,7 @@ class AbstractDownloadItem(QObject):
|
||||
elif isinstance(target, FileDownloadTarget):
|
||||
self._set_filename(
|
||||
target.filename, force_overwrite=target.force_overwrite)
|
||||
elif isinstance(target, OpenFileDownloadTarget):
|
||||
elif isinstance(target, (OpenFileDownloadTarget, PDFJSDownloadTarget)):
|
||||
try:
|
||||
fobj = temp_download_manager.get_tmpfile(self.basename)
|
||||
except OSError as exc:
|
||||
@ -749,8 +771,15 @@ class AbstractDownloadItem(QObject):
|
||||
message.error(msg)
|
||||
self.cancel()
|
||||
return
|
||||
self.finished.connect(
|
||||
functools.partial(self._open_if_successful, target.cmdline))
|
||||
|
||||
if isinstance(target, OpenFileDownloadTarget):
|
||||
self.finished.connect(functools.partial(
|
||||
self._open_if_successful, target.cmdline))
|
||||
elif isinstance(target, PDFJSDownloadTarget):
|
||||
self.finished.connect(self._pdfjs_if_successful)
|
||||
else:
|
||||
raise utils.Unreachable
|
||||
|
||||
self._set_tempfile(fobj)
|
||||
else: # pragma: no cover
|
||||
raise ValueError("Unsupported download target: {}".format(target))
|
||||
@ -797,6 +826,13 @@ class AbstractDownloadManager(QObject):
|
||||
dl.stats.update_speed()
|
||||
self.data_changed.emit(-1)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _on_pdfjs_requested(self, filename):
|
||||
"""Open PDF.js when a download requests it."""
|
||||
tabbed_browser = objreg.get('tabbed-browser', scope='window',
|
||||
window='last-focused')
|
||||
tabbed_browser.tabopen(pdfjs.get_main_url(filename))
|
||||
|
||||
def _init_item(self, download, auto_remove, suggested_filename):
|
||||
"""Initialize a newly created DownloadItem."""
|
||||
download.cancelled.connect(download.remove)
|
||||
@ -813,6 +849,8 @@ class AbstractDownloadManager(QObject):
|
||||
download.data_changed.connect(
|
||||
functools.partial(self._on_data_changed, download))
|
||||
download.error.connect(self._on_error)
|
||||
download.pdfjs_requested.connect(self._on_pdfjs_requested)
|
||||
|
||||
download.basename = suggested_filename
|
||||
idx = len(self.downloads)
|
||||
download.index = idx + 1 # "Human readable" index
|
||||
@ -1195,7 +1233,7 @@ class TempDownloadManager:
|
||||
"directory")
|
||||
self._tmpdir = None
|
||||
|
||||
def _get_tmpdir(self):
|
||||
def get_tmpdir(self):
|
||||
"""Return the temporary directory that is used for downloads.
|
||||
|
||||
The directory is created lazily on first access.
|
||||
@ -1221,13 +1259,13 @@ class TempDownloadManager:
|
||||
Return:
|
||||
A tempfile.NamedTemporaryFile that should be used to save the file.
|
||||
"""
|
||||
tmpdir = self._get_tmpdir()
|
||||
tmpdir = self.get_tmpdir()
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_name = utils.force_encoding(suggested_name, encoding)
|
||||
# Make sure that the filename is not too long
|
||||
suggested_name = utils.elide_filename(suggested_name, 50)
|
||||
fobj = tempfile.NamedTemporaryFile(dir=tmpdir.name, delete=False,
|
||||
suffix=suggested_name)
|
||||
suffix='_' + suggested_name)
|
||||
self.files.append(fobj)
|
||||
return fobj
|
||||
|
||||
|
@ -234,7 +234,7 @@ class GreasemonkeyManager(QObject):
|
||||
if not os.path.isfile(script_filename):
|
||||
continue
|
||||
script_path = os.path.join(scripts_dir, script_filename)
|
||||
with open(script_path, encoding='utf-8') as script_file:
|
||||
with open(script_path, encoding='utf-8-sig') as script_file:
|
||||
script = GreasemonkeyScript.parse(script_file.read())
|
||||
if not script.name:
|
||||
script.name = script_filename
|
||||
|
@ -693,7 +693,7 @@ class HintManager(QObject):
|
||||
- With `userscript`: The userscript to execute. Either store
|
||||
the userscript in
|
||||
`~/.local/share/qutebrowser/userscripts`
|
||||
(or `$XDG_DATA_DIR`), or use an absolute
|
||||
(or `$XDG_DATA_HOME`), or use an absolute
|
||||
path.
|
||||
- With `fill`: The command to fill the statusbar with.
|
||||
`{hint-url}` will get replaced by the selected
|
||||
|
@ -23,12 +23,12 @@ import os
|
||||
import time
|
||||
import contextlib
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, QTimer, pyqtSignal
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal
|
||||
from PyQt5.QtWidgets import QProgressDialog, QApplication
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.commands import cmdutils, cmdexc
|
||||
from qutebrowser.utils import (utils, objreg, log, usertypes, message,
|
||||
debug, standarddir, qtutils)
|
||||
from qutebrowser.utils import utils, objreg, log, usertypes, message, qtutils
|
||||
from qutebrowser.misc import objects, sql
|
||||
|
||||
|
||||
@ -36,6 +36,42 @@ from qutebrowser.misc import objects, sql
|
||||
_USER_VERSION = 2
|
||||
|
||||
|
||||
class HistoryProgress:
|
||||
|
||||
"""Progress dialog for history imports/conversions.
|
||||
|
||||
This makes WebHistory simpler as it can call methods of this class even
|
||||
when we don't want to show a progress dialog (for very small imports). This
|
||||
means tick() and finish() can be called even when start() wasn't.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._progress = None
|
||||
self._value = 0
|
||||
|
||||
def start(self, text, maximum):
|
||||
"""Start showing a progress dialog."""
|
||||
self._progress = QProgressDialog()
|
||||
self._progress.setMinimumDuration(500)
|
||||
self._progress.setLabelText(text)
|
||||
self._progress.setMaximum(maximum)
|
||||
self._progress.setCancelButton(None)
|
||||
self._progress.show()
|
||||
QApplication.processEvents()
|
||||
|
||||
def tick(self):
|
||||
"""Increase the displayed progress value."""
|
||||
self._value += 1
|
||||
if self._progress is not None:
|
||||
self._progress.setValue(self._value)
|
||||
QApplication.processEvents()
|
||||
|
||||
def finish(self):
|
||||
"""Finish showing the progress dialog."""
|
||||
if self._progress is not None:
|
||||
self._progress.hide()
|
||||
|
||||
|
||||
class CompletionMetaInfo(sql.SqlTable):
|
||||
|
||||
"""Table containing meta-information for the completion."""
|
||||
@ -86,20 +122,33 @@ class CompletionHistory(sql.SqlTable):
|
||||
|
||||
class WebHistory(sql.SqlTable):
|
||||
|
||||
"""The global history of visited pages."""
|
||||
"""The global history of visited pages.
|
||||
|
||||
Attributes:
|
||||
completion: A CompletionHistory instance.
|
||||
metainfo: A CompletionMetaInfo instance.
|
||||
_progress: A HistoryProgress instance.
|
||||
|
||||
Class attributes:
|
||||
_PROGRESS_THRESHOLD: When to start showing progress dialogs.
|
||||
"""
|
||||
|
||||
# All web history cleared
|
||||
history_cleared = pyqtSignal()
|
||||
# one url cleared
|
||||
url_cleared = pyqtSignal(QUrl)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
_PROGRESS_THRESHOLD = 1000
|
||||
|
||||
def __init__(self, progress, parent=None):
|
||||
super().__init__("History", ['url', 'title', 'atime', 'redirect'],
|
||||
constraints={'url': 'NOT NULL',
|
||||
'title': 'NOT NULL',
|
||||
'atime': 'NOT NULL',
|
||||
'redirect': 'NOT NULL'},
|
||||
parent=parent)
|
||||
self._progress = progress
|
||||
|
||||
self.completion = CompletionHistory(parent=self)
|
||||
self.metainfo = CompletionMetaInfo(parent=self)
|
||||
|
||||
@ -151,8 +200,8 @@ class WebHistory(sql.SqlTable):
|
||||
|
||||
def _is_excluded(self, url):
|
||||
"""Check if the given URL is excluded from the completion."""
|
||||
return any(pattern.matches(url)
|
||||
for pattern in config.val.completion.web_history.exclude)
|
||||
patterns = config.cache['completion.web_history.exclude']
|
||||
return any(pattern.matches(url) for pattern in patterns)
|
||||
|
||||
def _rebuild_completion(self):
|
||||
data = {'url': [], 'title': [], 'last_atime': []}
|
||||
@ -160,7 +209,14 @@ class WebHistory(sql.SqlTable):
|
||||
q = sql.Query('SELECT url, title, max(atime) AS atime FROM History '
|
||||
'WHERE NOT redirect and url NOT LIKE "qute://back%" '
|
||||
'GROUP BY url ORDER BY atime asc')
|
||||
for entry in q.run():
|
||||
entries = list(q.run())
|
||||
|
||||
if len(entries) > self._PROGRESS_THRESHOLD:
|
||||
self._progress.start("Rebuilding completion...", len(entries))
|
||||
|
||||
for entry in entries:
|
||||
self._progress.tick()
|
||||
|
||||
url = QUrl(entry.url)
|
||||
if self._is_excluded(url):
|
||||
continue
|
||||
@ -168,6 +224,8 @@ class WebHistory(sql.SqlTable):
|
||||
data['title'].append(entry.title)
|
||||
data['last_atime'].append(entry.atime)
|
||||
|
||||
self._progress.finish()
|
||||
|
||||
self.completion.insert_batch(data, replace=True)
|
||||
sql.Query('pragma user_version = {}'.format(_USER_VERSION)).run()
|
||||
|
||||
@ -283,108 +341,6 @@ class WebHistory(sql.SqlTable):
|
||||
'last_atime': atime
|
||||
}, replace=True)
|
||||
|
||||
def _parse_entry(self, line):
|
||||
"""Parse a history line like '12345 http://example.com title'."""
|
||||
if not line or line.startswith('#'):
|
||||
return None
|
||||
data = line.split(maxsplit=2)
|
||||
if len(data) == 2:
|
||||
atime, url = data
|
||||
title = ""
|
||||
elif len(data) == 3:
|
||||
atime, url, title = data
|
||||
else:
|
||||
raise ValueError("2 or 3 fields expected")
|
||||
|
||||
# http://xn--pple-43d.com/ with
|
||||
# https://bugreports.qt.io/browse/QTBUG-60364
|
||||
if url in ['http://.com/', 'https://.com/',
|
||||
'http://www..com/', 'https://www..com/']:
|
||||
return None
|
||||
|
||||
url = QUrl(url)
|
||||
if not url.isValid():
|
||||
raise ValueError("Invalid URL: {}".format(url.errorString()))
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/2646
|
||||
if url.scheme() == 'data':
|
||||
return None
|
||||
|
||||
# https://github.com/qutebrowser/qutebrowser/issues/670
|
||||
atime = atime.lstrip('\0')
|
||||
|
||||
if '-' in atime:
|
||||
atime, flags = atime.split('-')
|
||||
else:
|
||||
flags = ''
|
||||
|
||||
if not set(flags).issubset('r'):
|
||||
raise ValueError("Invalid flags {!r}".format(flags))
|
||||
|
||||
redirect = 'r' in flags
|
||||
return (url, title, int(atime), redirect)
|
||||
|
||||
def import_txt(self):
|
||||
"""Import a history text file into sqlite if it exists.
|
||||
|
||||
In older versions of qutebrowser, history was stored in a text format.
|
||||
This converts that file into the new sqlite format and moves it to a
|
||||
backup location.
|
||||
"""
|
||||
path = os.path.join(standarddir.data(), 'history')
|
||||
if not os.path.isfile(path):
|
||||
return
|
||||
|
||||
def action():
|
||||
"""Actually run the import."""
|
||||
with debug.log_time(log.init, 'Import old history file to sqlite'):
|
||||
try:
|
||||
self._read(path)
|
||||
except ValueError as ex:
|
||||
message.error('Failed to import history: {}'.format(ex))
|
||||
else:
|
||||
self._write_backup(path)
|
||||
|
||||
# delay to give message time to appear before locking down for import
|
||||
message.info('Converting {} to sqlite...'.format(path))
|
||||
QTimer.singleShot(100, action)
|
||||
|
||||
def _read(self, path):
|
||||
"""Import a text file into the sql database."""
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = {'url': [], 'title': [], 'atime': [], 'redirect': []}
|
||||
completion_data = {'url': [], 'title': [], 'last_atime': []}
|
||||
for (i, line) in enumerate(f):
|
||||
try:
|
||||
parsed = self._parse_entry(line.strip())
|
||||
if parsed is None:
|
||||
continue
|
||||
url, title, atime, redirect = parsed
|
||||
data['url'].append(self._format_url(url))
|
||||
data['title'].append(title)
|
||||
data['atime'].append(atime)
|
||||
data['redirect'].append(redirect)
|
||||
if not redirect:
|
||||
completion_data['url'].append(
|
||||
self._format_completion_url(url))
|
||||
completion_data['title'].append(title)
|
||||
completion_data['last_atime'].append(atime)
|
||||
except ValueError as ex:
|
||||
raise ValueError('Failed to parse line #{} of {}: "{}"'
|
||||
.format(i, path, ex))
|
||||
self.insert_batch(data)
|
||||
self.completion.insert_batch(completion_data, replace=True)
|
||||
|
||||
def _write_backup(self, path):
|
||||
bak = path + '.bak'
|
||||
message.info('History import complete. Appending {} to {}'
|
||||
.format(path, bak))
|
||||
with open(path, 'r', encoding='utf-8') as infile:
|
||||
with open(bak, 'a', encoding='utf-8') as outfile:
|
||||
for line in infile:
|
||||
outfile.write('\n' + line)
|
||||
os.remove(path)
|
||||
|
||||
def _format_url(self, url):
|
||||
return url.toString(QUrl.RemovePassword | QUrl.FullyEncoded)
|
||||
|
||||
@ -418,7 +374,8 @@ def init(parent=None):
|
||||
Args:
|
||||
parent: The parent to use for WebHistory.
|
||||
"""
|
||||
history = WebHistory(parent=parent)
|
||||
progress = HistoryProgress()
|
||||
history = WebHistory(progress=progress, parent=parent)
|
||||
objreg.register('web-history', history)
|
||||
|
||||
if objects.backend == usertypes.Backend.QtWebKit: # pragma: no cover
|
||||
|
@ -22,9 +22,11 @@
|
||||
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtCore import QUrl, QUrlQuery
|
||||
|
||||
from qutebrowser.utils import utils, javascript
|
||||
from qutebrowser.utils import utils, javascript, jinja, qtutils, usertypes
|
||||
from qutebrowser.misc import objects
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class PDFJSNotFound(Exception):
|
||||
@ -41,60 +43,54 @@ class PDFJSNotFound(Exception):
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
def generate_pdfjs_page(url):
|
||||
"""Return the html content of a page that displays url with pdfjs.
|
||||
def generate_pdfjs_page(filename, url):
|
||||
"""Return the html content of a page that displays a file with pdfjs.
|
||||
|
||||
Returns a string.
|
||||
|
||||
Args:
|
||||
url: The url of the pdf as QUrl.
|
||||
filename: The filename of the PDF to open.
|
||||
url: The URL being opened.
|
||||
"""
|
||||
if not is_available():
|
||||
return jinja.render('no_pdfjs.html',
|
||||
url=url.toDisplayString(),
|
||||
title="PDF.js not found")
|
||||
viewer = get_pdfjs_res('web/viewer.html').decode('utf-8')
|
||||
script = _generate_pdfjs_script(url)
|
||||
script = _generate_pdfjs_script(filename)
|
||||
html_page = viewer.replace('</body>',
|
||||
'</body><script>{}</script>'.format(script))
|
||||
return html_page
|
||||
|
||||
|
||||
def _generate_pdfjs_script(url):
|
||||
def _generate_pdfjs_script(filename):
|
||||
"""Generate the script that shows the pdf with pdf.js.
|
||||
|
||||
Args:
|
||||
url: The url of the pdf page as QUrl.
|
||||
filename: The name of the file to open.
|
||||
"""
|
||||
return (
|
||||
'document.addEventListener("DOMContentLoaded", function() {{\n'
|
||||
' PDFJS.verbosity = PDFJS.VERBOSITY_LEVELS.info;\n'
|
||||
' (window.PDFView || window.PDFViewerApplication).open("{url}");\n'
|
||||
'}});\n'
|
||||
).format(url=javascript.string_escape(url.toString(QUrl.FullyEncoded)))
|
||||
url = QUrl('qute://pdfjs/file')
|
||||
url_query = QUrlQuery()
|
||||
url_query.addQueryItem('filename', filename)
|
||||
url.setQuery(url_query)
|
||||
|
||||
return jinja.js_environment.from_string("""
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
{% if disable_create_object_url %}
|
||||
PDFJS.disableCreateObjectURL = true;
|
||||
{% endif %}
|
||||
PDFJS.verbosity = PDFJS.VERBOSITY_LEVELS.info;
|
||||
|
||||
def fix_urls(asset):
|
||||
"""Take an html page and replace each relative URL with an absolute.
|
||||
|
||||
This is specialized for pdf.js files and not a general purpose function.
|
||||
|
||||
Args:
|
||||
asset: js file or html page as string.
|
||||
"""
|
||||
new_urls = [
|
||||
('viewer.css', 'qute://pdfjs/web/viewer.css'),
|
||||
('compatibility.js', 'qute://pdfjs/web/compatibility.js'),
|
||||
('locale/locale.properties',
|
||||
'qute://pdfjs/web/locale/locale.properties'),
|
||||
('l10n.js', 'qute://pdfjs/web/l10n.js'),
|
||||
('../build/pdf.js', 'qute://pdfjs/build/pdf.js'),
|
||||
('debugger.js', 'qute://pdfjs/web/debugger.js'),
|
||||
('viewer.js', 'qute://pdfjs/web/viewer.js'),
|
||||
('compressed.tracemonkey-pldi-09.pdf', ''),
|
||||
('./images/', 'qute://pdfjs/web/images/'),
|
||||
('../build/pdf.worker.js', 'qute://pdfjs/build/pdf.worker.js'),
|
||||
('../web/cmaps/', 'qute://pdfjs/web/cmaps/'),
|
||||
]
|
||||
for original, new in new_urls:
|
||||
asset = asset.replace(original, new)
|
||||
return asset
|
||||
const viewer = window.PDFView || window.PDFViewerApplication;
|
||||
viewer.open("{{ url }}");
|
||||
});
|
||||
""").render(
|
||||
url=javascript.string_escape(url.toString(QUrl.FullyEncoded)),
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70420
|
||||
disable_create_object_url=(
|
||||
not qtutils.version_check('5.12') and
|
||||
not qtutils.version_check('5.7.1', exact=True, compiled=False) and
|
||||
objects.backend == usertypes.Backend.QtWebEngine))
|
||||
|
||||
|
||||
SYSTEM_PDFJS_PATHS = [
|
||||
@ -141,13 +137,7 @@ def get_pdfjs_res_and_path(path):
|
||||
except FileNotFoundError:
|
||||
raise PDFJSNotFound(path) from None
|
||||
|
||||
try:
|
||||
# Might be script/html or might be binary
|
||||
text_content = content.decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
return (content, file_path)
|
||||
text_content = fix_urls(text_content)
|
||||
return (text_content.encode('utf-8'), file_path)
|
||||
return content, file_path
|
||||
|
||||
|
||||
def get_pdfjs_res(path):
|
||||
@ -206,3 +196,22 @@ def is_available():
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
def should_use_pdfjs(mimetype, url):
|
||||
"""Check whether PDF.js should be used."""
|
||||
# e.g. 'blob:qute%3A///b45250b3-787e-44d1-a8d8-c2c90f81f981'
|
||||
is_download_url = (url.scheme() == 'blob' and
|
||||
QUrl(url.path()).scheme() == 'qute')
|
||||
is_pdf = mimetype in ['application/pdf', 'application/x-pdf']
|
||||
return is_pdf and not is_download_url and config.val.content.pdfjs
|
||||
|
||||
|
||||
def get_main_url(filename):
|
||||
"""Get the URL to be opened to view a local PDF."""
|
||||
url = QUrl('qute://pdfjs/web/viewer.html')
|
||||
query = QUrlQuery()
|
||||
query.addQueryItem('filename', filename) # read from our JS
|
||||
query.addQueryItem('file', '') # to avoid pdfjs opening the default PDF
|
||||
url.setQuery(query)
|
||||
return url
|
||||
|
@ -29,7 +29,6 @@ import json
|
||||
import os
|
||||
import time
|
||||
import textwrap
|
||||
import mimetypes
|
||||
import urllib
|
||||
import collections
|
||||
import base64
|
||||
@ -40,14 +39,13 @@ except ImportError:
|
||||
# New in Python 3.6
|
||||
secrets = None
|
||||
|
||||
import pkg_resources
|
||||
from PyQt5.QtCore import QUrlQuery, QUrl
|
||||
from PyQt5.QtCore import QUrlQuery, QUrl, qVersion
|
||||
|
||||
import qutebrowser
|
||||
from qutebrowser.browser import pdfjs, downloads
|
||||
from qutebrowser.config import config, configdata, configexc, configdiff
|
||||
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
|
||||
objreg, urlutils)
|
||||
from qutebrowser.misc import objects
|
||||
from qutebrowser.qt import sip
|
||||
|
||||
|
||||
@ -113,12 +111,10 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
|
||||
Attributes:
|
||||
_name: The 'foo' part of qute://foo
|
||||
backend: Limit which backends the handler can run with.
|
||||
"""
|
||||
|
||||
def __init__(self, name, backend=None):
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._backend = backend
|
||||
self._function = None
|
||||
|
||||
def __call__(self, function):
|
||||
@ -128,19 +124,7 @@ class add_handler: # noqa: N801,N806 pylint: disable=invalid-name
|
||||
|
||||
def wrapper(self, *args, **kwargs):
|
||||
"""Call the underlying function."""
|
||||
if self._backend is not None and objects.backend != self._backend:
|
||||
return self.wrong_backend_handler(*args, **kwargs)
|
||||
else:
|
||||
return self._function(*args, **kwargs)
|
||||
|
||||
def wrong_backend_handler(self, url):
|
||||
"""Show an error page about using the invalid backend."""
|
||||
src = jinja.render('error.html',
|
||||
title="Error while opening qute://url",
|
||||
url=url.toDisplayString(),
|
||||
error='{} is not available with this '
|
||||
'backend'.format(url.toDisplayString()))
|
||||
return 'text/html', src
|
||||
return self._function(*args, **kwargs)
|
||||
|
||||
|
||||
def data_for_url(url):
|
||||
@ -364,6 +348,23 @@ def qute_gpl(_url):
|
||||
return 'text/html', utils.read_file('html/license.html')
|
||||
|
||||
|
||||
def _asciidoc_fallback_path(html_path):
|
||||
"""Fall back to plaintext asciidoc if the HTML is unavailable."""
|
||||
asciidoc_path = html_path.replace('.html', '.asciidoc')
|
||||
asciidoc_paths = [asciidoc_path]
|
||||
if asciidoc_path.startswith('html/doc/'):
|
||||
asciidoc_paths += [asciidoc_path.replace('html/doc/', '../doc/help/'),
|
||||
asciidoc_path.replace('html/doc/', '../doc/')]
|
||||
|
||||
for path in asciidoc_paths:
|
||||
try:
|
||||
return utils.read_file(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@add_handler('help')
|
||||
def qute_help(url):
|
||||
"""Handler for qute://help."""
|
||||
@ -382,22 +383,13 @@ def qute_help(url):
|
||||
bdata = utils.read_file(path, binary=True)
|
||||
except OSError as e:
|
||||
raise SchemeOSError(e)
|
||||
mimetype, _encoding = mimetypes.guess_type(urlpath)
|
||||
assert mimetype is not None, url
|
||||
mimetype = utils.guess_mimetype(urlpath)
|
||||
return mimetype, bdata
|
||||
|
||||
try:
|
||||
data = utils.read_file(path)
|
||||
except OSError:
|
||||
# No .html around, let's see if we find the asciidoc
|
||||
asciidoc_path = path.replace('.html', '.asciidoc')
|
||||
if asciidoc_path.startswith('html/doc/'):
|
||||
asciidoc_path = asciidoc_path.replace('html/doc/', '../doc/help/')
|
||||
|
||||
try:
|
||||
asciidoc = utils.read_file(asciidoc_path)
|
||||
except OSError:
|
||||
asciidoc = None
|
||||
asciidoc = _asciidoc_fallback_path(path)
|
||||
|
||||
if asciidoc is None:
|
||||
raise
|
||||
@ -423,17 +415,6 @@ def qute_help(url):
|
||||
return 'text/html', data
|
||||
|
||||
|
||||
@add_handler('backend-warning')
|
||||
def qute_backend_warning(_url):
|
||||
"""Handler for qute://backend-warning."""
|
||||
src = jinja.render('backend-warning.html',
|
||||
distribution=version.distribution(),
|
||||
Distribution=version.Distribution,
|
||||
version=pkg_resources.parse_version,
|
||||
title="Legacy backend warning")
|
||||
return 'text/html', src
|
||||
|
||||
|
||||
def _qute_settings_set(url):
|
||||
"""Handler for qute://settings/set."""
|
||||
query = QUrlQuery(url)
|
||||
@ -531,3 +512,59 @@ def qute_pastebin_version(_url):
|
||||
"""Handler that pastebins the version string."""
|
||||
version.pastebin_version()
|
||||
return 'text/plain', b'Paste called.'
|
||||
|
||||
|
||||
@add_handler('pdfjs')
|
||||
def qute_pdfjs(url):
|
||||
"""Handler for qute://pdfjs. Return the pdf.js viewer."""
|
||||
if url.path() == '/file':
|
||||
filename = QUrlQuery(url).queryItemValue('filename')
|
||||
if not filename:
|
||||
raise UrlInvalidError("Missing filename")
|
||||
if '/' in filename or os.sep in filename:
|
||||
raise RequestDeniedError("Path separator in filename.")
|
||||
|
||||
path = os.path.join(downloads.temp_download_manager.get_tmpdir().name,
|
||||
filename)
|
||||
|
||||
with open(path, 'rb') as f:
|
||||
data = f.read()
|
||||
|
||||
mimetype = utils.guess_mimetype(filename, fallback=True)
|
||||
return mimetype, data
|
||||
|
||||
if url.path() == '/web/viewer.html':
|
||||
filename = QUrlQuery(url).queryItemValue("filename")
|
||||
if not filename:
|
||||
raise UrlInvalidError("Missing filename")
|
||||
data = pdfjs.generate_pdfjs_page(filename, url)
|
||||
return 'text/html', data
|
||||
|
||||
try:
|
||||
data = pdfjs.get_pdfjs_res(url.path())
|
||||
except pdfjs.PDFJSNotFound as e:
|
||||
# Logging as the error might get lost otherwise since we're not showing
|
||||
# the error page if a single asset is missing. This way we don't lose
|
||||
# information, as the failed pdfjs requests are still in the log.
|
||||
log.misc.warning(
|
||||
"pdfjs resource requested but not found: {}".format(e.path))
|
||||
raise NotFoundError("Can't find pdfjs resource '{}'".format(e.path))
|
||||
else:
|
||||
mimetype = utils.guess_mimetype(url.fileName(), fallback=True)
|
||||
return mimetype, data
|
||||
|
||||
|
||||
@add_handler('warning')
|
||||
def qute_warning(url):
|
||||
"""Handler for qute://warning."""
|
||||
path = url.path()
|
||||
if path == '/old-qt':
|
||||
src = jinja.render('warning-old-qt.html',
|
||||
title='Old Qt warning',
|
||||
qt_version=qVersion())
|
||||
elif path == '/webkit':
|
||||
src = jinja.render('warning-webkit.html',
|
||||
title='QtWebKit backend warning')
|
||||
else:
|
||||
raise NotFoundError("Invalid warning page {}".format(path))
|
||||
return 'text/html', src
|
||||
|
@ -27,7 +27,7 @@ import functools
|
||||
from PyQt5.QtCore import pyqtSlot, Qt
|
||||
from PyQt5.QtWebEngineWidgets import QWebEngineDownloadItem
|
||||
|
||||
from qutebrowser.browser import downloads
|
||||
from qutebrowser.browser import downloads, pdfjs
|
||||
from qutebrowser.utils import debug, usertypes, message, log, qtutils
|
||||
|
||||
|
||||
@ -221,6 +221,9 @@ class DownloadManager(downloads.AbstractDownloadManager):
|
||||
download.set_target(self._mhtml_target)
|
||||
self._mhtml_target = None
|
||||
return
|
||||
if pdfjs.should_use_pdfjs(qt_item.mimeType(), qt_item.url()):
|
||||
download.set_target(downloads.PDFJSDownloadTarget())
|
||||
return
|
||||
|
||||
filename = downloads.immediate_download_path()
|
||||
if filename is not None:
|
||||
|
@ -19,7 +19,7 @@
|
||||
|
||||
"""QtWebEngine specific qute://* handlers and glue code."""
|
||||
|
||||
from PyQt5.QtCore import QBuffer, QIODevice
|
||||
from PyQt5.QtCore import QBuffer, QIODevice, QUrl
|
||||
from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler,
|
||||
QWebEngineUrlRequestJob)
|
||||
|
||||
@ -39,6 +39,37 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
profile.installUrlSchemeHandler(b'chrome-error', self)
|
||||
profile.installUrlSchemeHandler(b'chrome-extension', self)
|
||||
|
||||
def _check_initiator(self, job):
|
||||
"""Check whether the initiator of the job should be allowed.
|
||||
|
||||
Only the browser itself or qute:// pages should access any of those
|
||||
URLs. The request interceptor further locks down qute://settings/set.
|
||||
|
||||
Args:
|
||||
job: QWebEngineUrlRequestJob
|
||||
|
||||
Return:
|
||||
True if the initiator is allowed, False if it was blocked.
|
||||
"""
|
||||
try:
|
||||
initiator = job.initiator()
|
||||
except AttributeError:
|
||||
# Added in Qt 5.11
|
||||
return True
|
||||
|
||||
if initiator == QUrl('null') and not qtutils.version_check('5.12'):
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70421
|
||||
return True
|
||||
|
||||
if initiator.isValid() and initiator.scheme() != 'qute':
|
||||
log.misc.warning("Blocking malicious request from {} to {}".format(
|
||||
initiator.toDisplayString(),
|
||||
job.requestUrl().toDisplayString()))
|
||||
job.fail(QWebEngineUrlRequestJob.RequestDenied)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def requestStarted(self, job):
|
||||
"""Handle a request for a qute: scheme.
|
||||
|
||||
@ -55,21 +86,8 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler):
|
||||
job.fail(QWebEngineUrlRequestJob.UrlInvalid)
|
||||
return
|
||||
|
||||
# Only the browser itself or qute:// pages should access any of those
|
||||
# URLs.
|
||||
# The request interceptor further locks down qute://settings/set.
|
||||
try:
|
||||
initiator = job.initiator()
|
||||
except AttributeError:
|
||||
# Added in Qt 5.11
|
||||
pass
|
||||
else:
|
||||
if initiator.isValid() and initiator.scheme() != 'qute':
|
||||
log.misc.warning("Blocking malicious request from {} to {}"
|
||||
.format(initiator.toDisplayString(),
|
||||
url.toDisplayString()))
|
||||
job.fail(QWebEngineUrlRequestJob.RequestDenied)
|
||||
return
|
||||
if not self._check_initiator(job):
|
||||
return
|
||||
|
||||
if job.requestMethod() != b'GET':
|
||||
job.fail(QWebEngineUrlRequestJob.RequestDenied)
|
||||
|
@ -166,8 +166,6 @@ class WebEngineSettings(websettings.AbstractSettings):
|
||||
# Qt 5.11
|
||||
'content.autoplay':
|
||||
('PlaybackRequiresUserGesture', lambda val: not val),
|
||||
'content.webrtc_public_interfaces_only':
|
||||
('WebRTCPublicInterfacesOnly', None),
|
||||
}
|
||||
for name, (attribute, converter) in new_attributes.items():
|
||||
try:
|
||||
|
@ -19,14 +19,12 @@
|
||||
|
||||
"""QtWebKit specific qute://* handlers and glue code."""
|
||||
|
||||
import mimetypes
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkAccessManager
|
||||
|
||||
from qutebrowser.browser import pdfjs, qutescheme
|
||||
from qutebrowser.browser import qutescheme
|
||||
from qutebrowser.browser.webkit.network import networkreply
|
||||
from qutebrowser.utils import log, usertypes, qtutils
|
||||
from qutebrowser.utils import log, qtutils
|
||||
|
||||
|
||||
def handler(request, operation, current_url):
|
||||
@ -81,22 +79,3 @@ def handler(request, operation, current_url):
|
||||
return networkreply.RedirectNetworkReply(e.url)
|
||||
|
||||
return networkreply.FixedDataNetworkReply(request, data, mimetype)
|
||||
|
||||
|
||||
@qutescheme.add_handler('pdfjs', backend=usertypes.Backend.QtWebKit)
|
||||
def qute_pdfjs(url):
|
||||
"""Handler for qute://pdfjs. Return the pdf.js viewer."""
|
||||
try:
|
||||
data = pdfjs.get_pdfjs_res(url.path())
|
||||
except pdfjs.PDFJSNotFound as e:
|
||||
# Logging as the error might get lost otherwise since we're not showing
|
||||
# the error page if a single asset is missing. This way we don't lose
|
||||
# information, as the failed pdfjs requests are still in the log.
|
||||
log.misc.warning(
|
||||
"pdfjs resource requested but not found: {}".format(e.path))
|
||||
raise qutescheme.NotFoundError("Can't find pdfjs resource '{}'".format(
|
||||
e.path))
|
||||
else:
|
||||
mimetype, _encoding = mimetypes.guess_type(url.fileName())
|
||||
assert mimetype is not None, url
|
||||
return mimetype, data
|
||||
|
@ -30,7 +30,7 @@ from PyQt5.QtPrintSupport import QPrintDialog
|
||||
from PyQt5.QtWebKitWidgets import QWebPage, QWebFrame
|
||||
|
||||
from qutebrowser.config import config
|
||||
from qutebrowser.browser import pdfjs, shared
|
||||
from qutebrowser.browser import pdfjs, shared, downloads
|
||||
from qutebrowser.browser.webkit import http
|
||||
from qutebrowser.browser.webkit.network import networkmanager
|
||||
from qutebrowser.utils import message, usertypes, log, jinja, objreg
|
||||
@ -206,18 +206,6 @@ class BrowserPage(QWebPage):
|
||||
suggested_file)
|
||||
return True
|
||||
|
||||
def _show_pdfjs(self, reply):
|
||||
"""Show the reply with pdfjs."""
|
||||
try:
|
||||
page = pdfjs.generate_pdfjs_page(reply.url())
|
||||
except pdfjs.PDFJSNotFound:
|
||||
page = jinja.render('no_pdfjs.html',
|
||||
url=reply.url().toDisplayString(),
|
||||
title="PDF.js not found")
|
||||
self.mainFrame().setContent(page.encode('utf-8'), 'text/html',
|
||||
reply.url())
|
||||
reply.deleteLater()
|
||||
|
||||
def shutdown(self):
|
||||
"""Prepare the web page for being deleted."""
|
||||
self._is_shutting_down = True
|
||||
@ -280,10 +268,9 @@ class BrowserPage(QWebPage):
|
||||
else:
|
||||
reply.finished.connect(functools.partial(
|
||||
self.display_content, reply, 'image/jpeg'))
|
||||
elif (mimetype in ['application/pdf', 'application/x-pdf'] and
|
||||
config.val.content.pdfjs):
|
||||
# Use pdf.js to display the page
|
||||
self._show_pdfjs(reply)
|
||||
elif pdfjs.should_use_pdfjs(mimetype, reply.url()):
|
||||
download_manager.fetch(reply,
|
||||
target=downloads.PDFJSDownloadTarget())
|
||||
else:
|
||||
# Unknown mimetype, so download anyways.
|
||||
download_manager.fetch(reply,
|
||||
|
@ -432,7 +432,7 @@ def run_async(tab, cmd, *args, win_id, env, verbose=False):
|
||||
cmd_path = os.path.expanduser(cmd)
|
||||
|
||||
# if cmd is not given as an absolute path, look it up
|
||||
# ~/.local/share/qutebrowser/userscripts (or $XDG_DATA_DIR)
|
||||
# ~/.local/share/qutebrowser/userscripts (or $XDG_DATA_HOME)
|
||||
if not os.path.isabs(cmd_path):
|
||||
log.misc.debug("{} is no absolute path".format(cmd_path))
|
||||
cmd_path = _lookup_path(cmd)
|
||||
|
@ -34,6 +34,7 @@ from qutebrowser.keyinput import keyutils
|
||||
val = None
|
||||
instance = None
|
||||
key_instance = None
|
||||
cache = None
|
||||
|
||||
# Keeping track of all change filters to validate them later.
|
||||
change_filters = []
|
||||
|
50
qutebrowser/config/configcache.py
Normal file
50
qutebrowser/config/configcache.py
Normal file
@ -0,0 +1,50 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2018 Jay Kamat <jaygkamat@gmail.com>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
"""Implementation of a basic config cache."""
|
||||
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
class ConfigCache:
|
||||
|
||||
"""A 'high-performance' cache for the config system.
|
||||
|
||||
Useful for areas which call out to the config system very frequently, DO
|
||||
NOT modify the value returned, DO NOT require per-url settings, do not
|
||||
change frequently, and do not require partially 'expanded' config paths.
|
||||
|
||||
If any of these requirements are broken, you will get incorrect or slow
|
||||
behavior.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._cache = {}
|
||||
config.instance.changed.connect(self._on_config_changed)
|
||||
|
||||
def _on_config_changed(self, attr: str) -> None:
|
||||
if attr in self._cache:
|
||||
self._cache[attr] = config.instance.get(attr)
|
||||
|
||||
def __getitem__(self, attr: str):
|
||||
if attr not in self._cache:
|
||||
assert not config.instance.get_opt(attr).supports_pattern
|
||||
self._cache[attr] = config.instance.get(attr)
|
||||
return self._cache[attr]
|
@ -181,6 +181,51 @@ qt.force_platform:
|
||||
This sets the `QT_QPA_PLATFORM` environment variable and is useful to force
|
||||
using the XCB plugin when running QtWebEngine on Wayland.
|
||||
|
||||
qt.process_model:
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- process-per-site-instance: Pages from separate sites are put into
|
||||
separate processes and separate visits to the same site are also
|
||||
isolated.
|
||||
- process-per-site: Pages from separate sites are put into separate
|
||||
processes. Unlike Process per Site Instance, all visits to the same
|
||||
site will share an OS process. The benefit of this model is reduced
|
||||
memory consumption, because more web pages will share processes.
|
||||
The drawbacks include reduced security, robustness, and
|
||||
responsiveness.
|
||||
- single-process: Run all tabs in a single process. This should be used
|
||||
for debugging purposes only, and it disables `:open --private`.
|
||||
default: process-per-site-instance
|
||||
backend: QtWebEngine
|
||||
restart: true
|
||||
desc: >-
|
||||
Which Chromium process model to use.
|
||||
|
||||
Alternative process models use less resources, but decrease security and
|
||||
robustness.
|
||||
|
||||
See the following pages for more details:
|
||||
|
||||
- https://www.chromium.org/developers/design-documents/process-models
|
||||
- https://doc.qt.io/qt-5/qtwebengine-features.html#process-models
|
||||
|
||||
qt.low_end_device_mode:
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- always: Always use low-end device mode.
|
||||
- auto: Decide automatically (uses low-end mode with < 1 GB available
|
||||
RAM).
|
||||
- never: Never use low-end device mode.
|
||||
default: auto
|
||||
backend: QtWebEngine
|
||||
restart: true
|
||||
desc: >-
|
||||
When to use Chromium's low-end device mode.
|
||||
|
||||
This improves the RAM usage of renderer processes, at the expense of
|
||||
performance.
|
||||
|
||||
qt.highdpi:
|
||||
type: Bool
|
||||
@ -220,10 +265,12 @@ content.autoplay:
|
||||
backend:
|
||||
QtWebEngine: Qt 5.10
|
||||
QtWebKit: false
|
||||
supports_pattern: true
|
||||
desc: >-
|
||||
Automatically start playing `<video>` elements.
|
||||
|
||||
Note this option needs a restart with QtWebEngine on Qt < 5.11.
|
||||
Note: On Qt < 5.11, this option needs a restart and does not support URL
|
||||
patterns.
|
||||
|
||||
content.cache.size:
|
||||
default: null
|
||||
@ -409,14 +456,18 @@ content.headers.referer:
|
||||
- never: "Never send the Referer. This is not recommended, as some sites
|
||||
may break."
|
||||
- same-domain: "Only send the Referer for the same domain. This will
|
||||
still protect your privacy, but shouldn't break any sites."
|
||||
backend: QtWebKit
|
||||
still protect your privacy, but shouldn't break any sites. With
|
||||
QtWebEngine, the referer will still be sent for other domains, but
|
||||
with stripped path information."
|
||||
restart: true
|
||||
desc: >-
|
||||
When to send the Referer header.
|
||||
|
||||
The Referer header tells websites from which website you were coming from
|
||||
when visiting them.
|
||||
|
||||
No restart is needed with QtWebKit.
|
||||
|
||||
content.headers.user_agent:
|
||||
default: null
|
||||
type:
|
||||
@ -501,13 +552,10 @@ content.host_blocking.whitelist:
|
||||
- piwik.org
|
||||
type:
|
||||
name: List
|
||||
valtype: String
|
||||
valtype: UrlPattern
|
||||
none_ok: true
|
||||
desc: >-
|
||||
List of domains that should always be loaded, despite being ad-blocked.
|
||||
|
||||
Domains may contain * and ? wildcards and are otherwise required to exactly
|
||||
match the requested domain.
|
||||
A list of patterns that should always be loaded, despite being ad-blocked.
|
||||
|
||||
Local domains are always exempt from hostblocking.
|
||||
|
||||
@ -639,7 +687,6 @@ content.notifications:
|
||||
content.pdfjs:
|
||||
default: false
|
||||
type: Bool
|
||||
backend: QtWebKit
|
||||
desc: >-
|
||||
Allow pdf.js to view PDF files in the browser.
|
||||
|
||||
@ -723,18 +770,31 @@ content.webgl:
|
||||
supports_pattern: true
|
||||
desc: Enable WebGL.
|
||||
|
||||
content.webrtc_public_interfaces_only:
|
||||
default: false
|
||||
type: Bool
|
||||
content.webrtc_ip_handling_policy:
|
||||
default: all-interfaces
|
||||
type:
|
||||
name: String
|
||||
valid_values:
|
||||
- all-interfaces: WebRTC has the right to enumerate all interfaces and
|
||||
bind them to discover public interfaces.
|
||||
- default-public-and-private-interfaces: WebRTC should only use the
|
||||
default route used by http. This also exposes the associated
|
||||
default private address. Default route is the route chosen by the
|
||||
OS on a multi-homed endpoint.
|
||||
- default-public-interface-only: WebRTC should only use the default route
|
||||
used by http. This doesn't expose any local addresses.
|
||||
- disable-non-proxied-udp: WebRTC should only use TCP to contact peers or
|
||||
servers unless the proxy server supports UDP. This doesn't expose
|
||||
any local addresses either.
|
||||
default: all-interfaces
|
||||
backend:
|
||||
QtWebKit: false
|
||||
QtWebEngine: Qt 5.9.2
|
||||
restart: true
|
||||
desc: >-
|
||||
Only expose public interfaces via WebRTC.
|
||||
Which interfaces to expose via WebRTC.
|
||||
|
||||
On Qt 5.9, this option requires a restart.
|
||||
On Qt 5.10, this option doesn't work at all because of a Qt bug.
|
||||
On Qt >= 5.11, no restart is required.
|
||||
On Qt 5.10, this option doesn't work because of a Qt bug.
|
||||
|
||||
content.xss_auditing:
|
||||
type: Bool
|
||||
|
@ -276,6 +276,21 @@ class YamlConfig(QObject):
|
||||
del settings['bindings.default']
|
||||
self._mark_changed()
|
||||
|
||||
# content.webrtc_public_interfaces_only got merged into
|
||||
# content.webrtc_ip_handling_policy.
|
||||
old = 'content.webrtc_public_interfaces_only'
|
||||
new = 'content.webrtc_ip_handling_policy'
|
||||
if old in settings:
|
||||
settings[new] = {}
|
||||
for scope, val in settings[old].items():
|
||||
if val:
|
||||
settings[new][scope] = 'default-public-interface-only'
|
||||
else:
|
||||
settings[new][scope] = 'all-interfaces'
|
||||
|
||||
del settings[old]
|
||||
self._mark_changed()
|
||||
|
||||
self._migrate_bool(settings, 'tabs.favicons.show', 'always', 'never')
|
||||
self._migrate_bool(settings, 'qt.force_software_rendering',
|
||||
'software-opengl', 'none')
|
||||
|
@ -28,6 +28,7 @@ from qutebrowser.config import (config, configdata, configfiles, configtypes,
|
||||
configexc, configcommands)
|
||||
from qutebrowser.utils import (objreg, usertypes, log, standarddir, message,
|
||||
qtutils)
|
||||
from qutebrowser.config import configcache
|
||||
from qutebrowser.misc import msgbox, objects
|
||||
|
||||
|
||||
@ -44,6 +45,7 @@ def early_init(args):
|
||||
config.instance = config.Config(yaml_config=yaml_config)
|
||||
config.val = config.ConfigContainer(config.instance)
|
||||
config.key_instance = config.KeyConfig(config.instance)
|
||||
config.cache = configcache.ConfigCache()
|
||||
yaml_config.setParent(config.instance)
|
||||
|
||||
for cf in config.change_filters:
|
||||
@ -169,24 +171,67 @@ def qt_args(namespace):
|
||||
argv += ['--' + arg for arg in config.val.qt.args]
|
||||
|
||||
if objects.backend == usertypes.Backend.QtWebEngine:
|
||||
if not qtutils.version_check('5.11', compiled=False):
|
||||
# WORKAROUND equivalent to
|
||||
# https://codereview.qt-project.org/#/c/217932/
|
||||
# Needed for Qt < 5.9.5 and < 5.10.1
|
||||
argv.append('--disable-shared-workers')
|
||||
|
||||
if config.val.qt.force_software_rendering == 'chromium':
|
||||
argv.append('--disable-gpu')
|
||||
|
||||
if not config.val.content.canvas_reading:
|
||||
argv.append('--disable-reading-from-canvas')
|
||||
|
||||
if not qtutils.version_check('5.11'):
|
||||
# On Qt 5.11, we can control this via QWebEngineSettings
|
||||
if not config.val.content.autoplay:
|
||||
argv.append('--autoplay-policy=user-gesture-required')
|
||||
if config.val.content.webrtc_public_interfaces_only:
|
||||
argv.append('--force-webrtc-ip-handling-policy='
|
||||
'default_public_interface_only')
|
||||
argv += list(_qtwebengine_args())
|
||||
|
||||
return argv
|
||||
|
||||
|
||||
def _qtwebengine_args():
|
||||
"""Get the QtWebEngine arguments to use based on the config."""
|
||||
if not qtutils.version_check('5.11', compiled=False):
|
||||
# WORKAROUND equivalent to
|
||||
# https://codereview.qt-project.org/#/c/217932/
|
||||
# Needed for Qt < 5.9.5 and < 5.10.1
|
||||
yield '--disable-shared-workers'
|
||||
|
||||
settings = {
|
||||
'qt.force_software_rendering': {
|
||||
'software-opengl': None,
|
||||
'qt-quick': None,
|
||||
'chromium': '--disable-gpu',
|
||||
'none': None,
|
||||
},
|
||||
'content.canvas_reading': {
|
||||
True: None,
|
||||
False: '--disable-reading-from-canvas',
|
||||
},
|
||||
'content.webrtc_ip_handling_policy': {
|
||||
'all-interfaces': None,
|
||||
'default-public-and-private-interfaces':
|
||||
'--force-webrtc-ip-handling-policy='
|
||||
'default_public_and_private_interfaces',
|
||||
'default-public-interface-only':
|
||||
'--force-webrtc-ip-handling-policy='
|
||||
'default_public_interface_only',
|
||||
'disable-non-proxied-udp':
|
||||
'--force-webrtc-ip-handling-policy='
|
||||
'disable_non_proxied_udp',
|
||||
},
|
||||
'qt.process_model': {
|
||||
'process-per-site-instance': None,
|
||||
'process-per-site': '--process-per-site',
|
||||
'single-process': '--single-process',
|
||||
},
|
||||
'qt.low_end_device_mode': {
|
||||
'auto': None,
|
||||
'always': '--enable-low-end-device-mode',
|
||||
'never': '--disable-low-end-device-mode',
|
||||
},
|
||||
'content.headers.referer': {
|
||||
'always': None,
|
||||
'never': '--no-referrers',
|
||||
'same-domain': '--reduced-referrer-granularity',
|
||||
}
|
||||
}
|
||||
|
||||
if not qtutils.version_check('5.11'):
|
||||
# On Qt 5.11, we can control this via QWebEngineSettings
|
||||
settings['content.autoplay'] = {
|
||||
True: None,
|
||||
False: '--autoplay-policy=user-gesture-required',
|
||||
}
|
||||
|
||||
for setting, args in sorted(settings.items()):
|
||||
arg = args[config.instance.get(setting)]
|
||||
if arg is not None:
|
||||
yield arg
|
||||
|
@ -24,6 +24,10 @@ h1 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
@ -45,4 +49,13 @@ td {
|
||||
margin-left: 10px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.note {
|
||||
font-size: smaller;
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: monospace;
|
||||
}
|
||||
{% endblock %}
|
||||
|
24
qutebrowser/html/warning-old-qt.html
Normal file
24
qutebrowser/html/warning-old-qt.html
Normal file
@ -0,0 +1,24 @@
|
||||
{% extends "styled.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ title }}</h1>
|
||||
<span class="note">Note this warning will only appear once. Use <span class="mono">:open
|
||||
qute://warning/old-qt</span> to show it again at a later time.</span>
|
||||
|
||||
<p>You're using qutebrowser with Qt {{qt_version}}.</p>
|
||||
|
||||
<p>Qt 5.7 was released in June 2016, with the 5.7.1 patch release in December
|
||||
2016. It is based on Chromium 49 (March 2016) with (some) security fixes up to
|
||||
Chromium 54 (October 2016). It is also
|
||||
<a href="https://www.debian.org/releases/stable/amd64/release-notes/ch-information.en.html#browser-security">not covered</a>
|
||||
by Debian security updates.</p>
|
||||
|
||||
<p>Qt 5.8 has had various bugs, and has been unsupported (but working to some
|
||||
degree) in qutebrowser for a while.</p>
|
||||
|
||||
<p>Because of those security issues and the maintaince burden coming with
|
||||
supporting old versions, support for Qt < 5.9 will be dropped in a future
|
||||
qutebrowser release. You might want to check
|
||||
<a href="https://qutebrowser.org/doc/install.html">alternate installation methods</a>
|
||||
which allow you to get a newer Qt.</p>
|
||||
{% endblock %}
|
82
qutebrowser/html/warning-webkit.html
Normal file
82
qutebrowser/html/warning-webkit.html
Normal file
@ -0,0 +1,82 @@
|
||||
{% extends "styled.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ title }}</h1>
|
||||
<span class="note">Note this warning will only appear once. Use <span class="mono">:open
|
||||
qute://warning/webkit</span> to show it again at a later time.</span>
|
||||
|
||||
<p>You're using qutebrowser with the QtWebKit backend.</p>
|
||||
|
||||
<p>Unfortunately, QtWebKit hasn't seen a release (including security updates)
|
||||
since June 2017, and it also lacks various security features (process
|
||||
isolation/sandboxing) present in QtWebEngine.</p>
|
||||
|
||||
<p>Because of those security issues and the maintaince burden coming with
|
||||
supporting QtWebKit, support for it will be dropped in a future qutebrowser
|
||||
release. It's recommended that you use QtWebEngine instead.</p>
|
||||
|
||||
<h2>(Outdated) reasons to use QtWebKit</h2>
|
||||
<p>Most reasons why people preferred the QtWebKit backend aren't relevant anymore:</p>
|
||||
|
||||
<p><b>PDF.js support</b>: This qutebrowser release comes with PDF.js support
|
||||
for QtWebEngine.</p>
|
||||
|
||||
<p><b>Missing control over Referer header</b>: This qutebrowser release
|
||||
supports <span class="mono">content.headers.referer</span> for QtWebEngine.</p>
|
||||
|
||||
<p><b>Missing control over cookies</b>: With Qt 5.11 or newer, the <span
|
||||
class="mono">content.cookies.accept</span> setting works on QtWebEngine.</p>
|
||||
|
||||
<p><b>Graphical glitches</b>: The new values for the <span
|
||||
class="mono">qt.force_software_rendering</span> setting added in v1.4.0 should
|
||||
hopefully help.</p>
|
||||
|
||||
<p><b>Missing support for notifications</b>: Those <a
|
||||
href="https://bugreports.qt.io/browse/QTBUG-29611">aren't supported yet</a> in
|
||||
Qt, but support is planned to be added in Qt 5.13, released around May 2019.</p>
|
||||
|
||||
<p><b>Resource usage</b>: This release adds the <span
|
||||
class="mono">qt.process_model</span> and <span
|
||||
class="mono">qt.low_end_device_mode</span> settings which can be used to
|
||||
decrease the resource usage of QtWebEngine (but come with other drawbacks).</p>
|
||||
|
||||
<p><b>Not trusting Google</b>: Various people have checked the connections made
|
||||
by QtWebEngine/qutebrowser, and it doesn't make any connections to Google (or
|
||||
any other unsolicited connections at all). Arguably, having to trust Google
|
||||
also is a smaller issue than having to trust every website you visit because of
|
||||
heaps of security issues...</p>
|
||||
|
||||
<p><b>Nouveau graphic driver</b>: You can use QtWebEngine with software
|
||||
rendering. With Qt 5.13 (~May 2019) it might be possible to run with Nouveau
|
||||
without software rendering.</p>
|
||||
|
||||
<p><b>Wayland</b>: It's possible to use QtWebEngine with XWayland. Some users
|
||||
also seem to be able to run it natively with Qt 5.11, but currently, <span
|
||||
class="mono">QUTE_SKIP_WAYLAND_CHECK=1</span> needs to be set in the
|
||||
environment to do so.</p>
|
||||
|
||||
<p><b>Instability on FreeBSD</b>: Those seem to be FreeBSD-specific crashes,
|
||||
and unfortunately nobody has looked into them yet so far...</p>
|
||||
|
||||
<p><b>QtWebEngine being unavailable in ArchlinuxARM's PyQt package</b>:
|
||||
QtWebEngine itself is available on the armv7h/aarch64 architectures, but their
|
||||
PyQt package is broken and doesn't come with QtWebEngine support. This
|
||||
<a href="https://archlinuxarm.org/forum/viewtopic.php?f=15&t=11269&p=54587">has
|
||||
been reported</a> in their forums, but without any change so far. It should
|
||||
however be possible to rebuild the PyQt package from source with QtWebEngine
|
||||
installed.</p>
|
||||
|
||||
<p><b>QtWebEngine being unavailable on Parabola</b>: Claims of Parabola
|
||||
developers about QtWebEngine being "non-free" have repeatedly been disputed,
|
||||
and so far nobody came up with solid evidence about that being the case. Also,
|
||||
note that their qutebrowser package is orphaned and was often outdated in the
|
||||
past (even qutebrowser security fixes took months to arrive there). You
|
||||
might be better off chosing an <a
|
||||
href="https://qutebrowser.org/doc/install.html">alternative install
|
||||
method</a>.</p>
|
||||
|
||||
<p><b>White flashing between loads with a custom stylesheet</b>: This doesn't
|
||||
seem to happen with <span class="mono">qt.process_model = single-process</span>
|
||||
set. However, note that that setting comes with decreased security and
|
||||
stability, but QtWebKit doesn't have any process isolation at all.</p>
|
||||
{% endblock %}
|
@ -719,9 +719,9 @@ class TabbedBrowser(QWidget):
|
||||
except TabDeletedError:
|
||||
# We can get signals for tabs we already deleted...
|
||||
return
|
||||
start = config.val.colors.tabs.indicator.start
|
||||
stop = config.val.colors.tabs.indicator.stop
|
||||
system = config.val.colors.tabs.indicator.system
|
||||
start = config.cache['colors.tabs.indicator.start']
|
||||
stop = config.cache['colors.tabs.indicator.stop']
|
||||
system = config.cache['colors.tabs.indicator.system']
|
||||
color = utils.interpolate_color(start, stop, perc, system)
|
||||
self.widget.set_tab_indicator_color(idx, color)
|
||||
self.widget.update_tab_title(idx)
|
||||
|
@ -139,9 +139,9 @@ class TabWidget(QTabWidget):
|
||||
"""
|
||||
tab = self.widget(idx)
|
||||
if tab.data.pinned:
|
||||
fmt = config.val.tabs.title.format_pinned
|
||||
fmt = config.cache['tabs.title.format_pinned']
|
||||
else:
|
||||
fmt = config.val.tabs.title.format
|
||||
fmt = config.cache['tabs.title.format']
|
||||
|
||||
if (field is not None and
|
||||
(fmt is None or ('{' + field + '}') not in fmt)):
|
||||
@ -604,7 +604,7 @@ class TabBar(QTabBar):
|
||||
minimum_size = self.minimumTabSizeHint(index)
|
||||
height = minimum_size.height()
|
||||
if self.vertical:
|
||||
confwidth = str(config.val.tabs.width)
|
||||
confwidth = str(config.cache['tabs.width'])
|
||||
if confwidth.endswith('%'):
|
||||
main_window = objreg.get('main-window', scope='window',
|
||||
window=self._win_id)
|
||||
@ -614,7 +614,7 @@ class TabBar(QTabBar):
|
||||
width = int(confwidth)
|
||||
size = QSize(max(minimum_size.width(), width), height)
|
||||
else:
|
||||
if config.val.tabs.pinned.shrink:
|
||||
if config.cache['tabs.pinned.shrink']:
|
||||
pinned = self._tab_pinned(index)
|
||||
pinned_count, pinned_width = self._pinned_statistics()
|
||||
else:
|
||||
@ -652,15 +652,15 @@ class TabBar(QTabBar):
|
||||
tab = QStyleOptionTab()
|
||||
self.initStyleOption(tab, idx)
|
||||
|
||||
# pylint: disable=bad-config-option
|
||||
setting = config.val.colors.tabs
|
||||
# pylint: enable=bad-config-option
|
||||
setting = 'colors.tabs'
|
||||
if idx == selected:
|
||||
setting = setting.selected
|
||||
setting = setting.odd if (idx + 1) % 2 else setting.even
|
||||
setting += '.selected'
|
||||
setting += '.odd' if (idx + 1) % 2 else '.even'
|
||||
|
||||
tab.palette.setColor(QPalette.Window, setting.bg)
|
||||
tab.palette.setColor(QPalette.WindowText, setting.fg)
|
||||
tab.palette.setColor(QPalette.Window,
|
||||
config.cache[setting + '.bg'])
|
||||
tab.palette.setColor(QPalette.WindowText,
|
||||
config.cache[setting + '.fg'])
|
||||
|
||||
indicator_color = self.tab_indicator_color(idx)
|
||||
tab.palette.setColor(QPalette.Base, indicator_color)
|
||||
@ -805,7 +805,7 @@ class TabBarStyle(QCommonStyle):
|
||||
elif element == QStyle.CE_TabBarTabLabel:
|
||||
if not opt.icon.isNull() and layouts.icon.isValid():
|
||||
self._draw_icon(layouts, opt, p)
|
||||
alignment = (config.val.tabs.title.alignment |
|
||||
alignment = (config.cache['tabs.title.alignment'] |
|
||||
Qt.AlignVCenter | Qt.TextHideMnemonic)
|
||||
self._style.drawItemText(p, layouts.text, alignment, opt.palette,
|
||||
opt.state & QStyle.State_Enabled,
|
||||
@ -878,8 +878,8 @@ class TabBarStyle(QCommonStyle):
|
||||
Return:
|
||||
A Layout object with two QRects.
|
||||
"""
|
||||
padding = config.val.tabs.padding
|
||||
indicator_padding = config.val.tabs.indicator.padding
|
||||
padding = config.cache['tabs.padding']
|
||||
indicator_padding = config.cache['tabs.indicator.padding']
|
||||
|
||||
text_rect = QRect(opt.rect)
|
||||
if not text_rect.isValid():
|
||||
@ -890,7 +890,7 @@ class TabBarStyle(QCommonStyle):
|
||||
text_rect.adjust(padding.left, padding.top, -padding.right,
|
||||
-padding.bottom)
|
||||
|
||||
indicator_width = config.val.tabs.indicator.width
|
||||
indicator_width = config.cache['tabs.indicator.width']
|
||||
if indicator_width == 0:
|
||||
indicator_rect = QRect()
|
||||
else:
|
||||
@ -933,9 +933,9 @@ class TabBarStyle(QCommonStyle):
|
||||
icon_state = (QIcon.On if opt.state & QStyle.State_Selected
|
||||
else QIcon.Off)
|
||||
# reserve space for favicon when tab bar is vertical (issue #1968)
|
||||
position = config.val.tabs.position
|
||||
position = config.cache['tabs.position']
|
||||
if (position in [QTabWidget.East, QTabWidget.West] and
|
||||
config.val.tabs.favicons.show != 'never'):
|
||||
config.cache['tabs.favicons.show'] != 'never'):
|
||||
tab_icon_size = icon_size
|
||||
else:
|
||||
actual_size = opt.icon.actualSize(icon_size, icon_mode, icon_state)
|
||||
|
@ -68,6 +68,11 @@ def _other_backend(backend):
|
||||
def _error_text(because, text, backend):
|
||||
"""Get an error text for the given information."""
|
||||
other_backend, other_setting = _other_backend(backend)
|
||||
if other_backend == usertypes.Backend.QtWebKit:
|
||||
warning = ("<i>Note that QtWebKit hasn't been updated since "
|
||||
"July 2017 (including security updates).</i>")
|
||||
else:
|
||||
warning = ""
|
||||
return ("<b>Failed to start with the {backend} backend!</b>"
|
||||
"<p>qutebrowser tried to start with the {backend} backend but "
|
||||
"failed because {because}.</p>{text}"
|
||||
@ -75,9 +80,10 @@ def _error_text(because, text, backend):
|
||||
"<p>This forces usage of the {other_backend.name} backend by "
|
||||
"setting the <i>backend = '{other_setting}'</i> option "
|
||||
"(if you have a <i>config.py</i> file, you'll need to set "
|
||||
"this manually).</p>".format(
|
||||
"this manually). {warning}</p>".format(
|
||||
backend=backend.name, because=because, text=text,
|
||||
other_backend=other_backend, other_setting=other_setting))
|
||||
other_backend=other_backend, other_setting=other_setting,
|
||||
warning=warning))
|
||||
|
||||
|
||||
class _Dialog(QDialog):
|
||||
@ -102,8 +108,10 @@ class _Dialog(QDialog):
|
||||
quit_button.clicked.connect(lambda: self.done(_Result.quit))
|
||||
hbox.addWidget(quit_button)
|
||||
|
||||
backend_button = QPushButton("Force {} backend".format(
|
||||
other_backend.name))
|
||||
backend_text = "Force {} backend".format(other_backend.name)
|
||||
if other_backend == usertypes.Backend.QtWebKit:
|
||||
backend_text += ' (not recommended)'
|
||||
backend_button = QPushButton(backend_text)
|
||||
backend_button.clicked.connect(functools.partial(
|
||||
self._change_setting, 'backend', other_setting))
|
||||
hbox.addWidget(backend_button)
|
||||
|
@ -35,6 +35,7 @@ class SqliteErrorCode:
|
||||
in qutebrowser here.
|
||||
"""
|
||||
|
||||
UNKNOWN = '-1'
|
||||
BUSY = '5' # database is locked
|
||||
READONLY = '8' # attempt to write a readonly database
|
||||
IOERR = '10' # disk I/O error
|
||||
@ -86,12 +87,17 @@ class SqlBugError(SqlError):
|
||||
|
||||
def raise_sqlite_error(msg, error):
|
||||
"""Raise either a SqlBugError or SqlEnvironmentError."""
|
||||
error_code = error.nativeErrorCode()
|
||||
database_text = error.databaseText()
|
||||
driver_text = error.driverText()
|
||||
|
||||
log.sql.debug("SQL error:")
|
||||
log.sql.debug("type: {}".format(
|
||||
debug.qenum_key(QSqlError, error.type())))
|
||||
log.sql.debug("database text: {}".format(error.databaseText()))
|
||||
log.sql.debug("driver text: {}".format(error.driverText()))
|
||||
log.sql.debug("error code: {}".format(error.nativeErrorCode()))
|
||||
log.sql.debug("database text: {}".format(database_text))
|
||||
log.sql.debug("driver text: {}".format(driver_text))
|
||||
log.sql.debug("error code: {}".format(error_code))
|
||||
|
||||
environmental_errors = [
|
||||
SqliteErrorCode.BUSY,
|
||||
SqliteErrorCode.READONLY,
|
||||
@ -100,17 +106,15 @@ def raise_sqlite_error(msg, error):
|
||||
SqliteErrorCode.FULL,
|
||||
SqliteErrorCode.CANTOPEN,
|
||||
]
|
||||
# At least in init(), we can get errors like this:
|
||||
# > type: ConnectionError
|
||||
# > database text: out of memory
|
||||
# > driver text: Error opening database
|
||||
# > error code: -1
|
||||
environmental_strings = [
|
||||
"out of memory",
|
||||
]
|
||||
errcode = error.nativeErrorCode()
|
||||
if (errcode in environmental_errors or
|
||||
(errcode == -1 and error.databaseText() in environmental_strings)):
|
||||
|
||||
# WORKAROUND for https://bugreports.qt.io/browse/QTBUG-70506
|
||||
# We don't know what the actual error was, but let's assume it's not us to
|
||||
# blame... Usually this is something like an unreadable database file.
|
||||
qtbug_70506 = (error_code == SqliteErrorCode.UNKNOWN and
|
||||
driver_text == "Error opening database" and
|
||||
database_text == "out of memory")
|
||||
|
||||
if error_code in environmental_errors or qtbug_70506:
|
||||
raise SqlEnvironmentError(msg, error)
|
||||
else:
|
||||
raise SqlBugError(msg, error)
|
||||
|
@ -22,7 +22,6 @@
|
||||
import os
|
||||
import os.path
|
||||
import contextlib
|
||||
import mimetypes
|
||||
import html
|
||||
|
||||
import jinja2
|
||||
@ -108,9 +107,8 @@ class Environment(jinja2.Environment):
|
||||
"""Get a data: url for the broken qutebrowser logo."""
|
||||
data = utils.read_file(path, binary=True)
|
||||
filename = utils.resource_filename(path)
|
||||
mimetype = mimetypes.guess_type(filename)
|
||||
assert mimetype is not None, path
|
||||
return urlutils.data_url(mimetype[0], data).toString()
|
||||
mimetype = utils.guess_mimetype(filename)
|
||||
return urlutils.data_url(mimetype, data).toString()
|
||||
|
||||
def getattr(self, obj, attribute):
|
||||
"""Override jinja's getattr() to be less clever.
|
||||
|
@ -33,6 +33,7 @@ import contextlib
|
||||
import socket
|
||||
import shlex
|
||||
import glob
|
||||
import mimetypes
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QColor, QClipboard, QDesktopServices
|
||||
@ -683,3 +684,19 @@ def chunk(elems, n):
|
||||
raise ValueError("n needs to be at least 1!")
|
||||
for i in range(0, len(elems), n):
|
||||
yield elems[i:i + n]
|
||||
|
||||
|
||||
def guess_mimetype(filename, fallback=False):
|
||||
"""Guess a mimetype based on a filename.
|
||||
|
||||
Args:
|
||||
filename: The filename to check.
|
||||
fallback: Fall back to application/octet-stream if unknown.
|
||||
"""
|
||||
mimetype, _encoding = mimetypes.guess_type(filename)
|
||||
if mimetype is None:
|
||||
if fallback:
|
||||
return 'application/octet-stream'
|
||||
else:
|
||||
raise ValueError("Got None mimetype for {}".format(filename))
|
||||
return mimetype
|
||||
|
@ -209,15 +209,6 @@ def build_mac():
|
||||
return [(dmg_name, 'application/x-apple-diskimage', 'macOS .dmg')]
|
||||
|
||||
|
||||
def patch_windows(out_dir):
|
||||
"""Copy missing DLLs for windows into the given output."""
|
||||
dll_dir = os.path.join('.tox', 'pyinstaller', 'lib', 'site-packages',
|
||||
'PyQt5', 'Qt', 'bin')
|
||||
dlls = ['libEGL.dll', 'libGLESv2.dll', 'libeay32.dll', 'ssleay32.dll']
|
||||
for dll in dlls:
|
||||
shutil.copy(os.path.join(dll_dir, dll), out_dir)
|
||||
|
||||
|
||||
def build_windows():
|
||||
"""Build windows executables/setups."""
|
||||
utils.print_title("Updating 3rdparty content")
|
||||
@ -252,7 +243,9 @@ def build_windows():
|
||||
_maybe_remove(out_64)
|
||||
call_tox('pyinstaller', '-r', python=python_x64)
|
||||
shutil.move(out_pyinstaller, out_64)
|
||||
patch_windows(out_64)
|
||||
|
||||
utils.print_title("Running 64bit smoke test")
|
||||
smoke_test(os.path.join(out_64, 'qutebrowser.exe'))
|
||||
|
||||
utils.print_title("Building installers")
|
||||
subprocess.run(['makensis.exe',
|
||||
@ -268,9 +261,6 @@ def build_windows():
|
||||
'Windows 64bit installer'),
|
||||
]
|
||||
|
||||
utils.print_title("Running 64bit smoke test")
|
||||
smoke_test(os.path.join(out_64, 'qutebrowser.exe'))
|
||||
|
||||
utils.print_title("Zipping 64bit standalone...")
|
||||
name = 'qutebrowser-{}-windows-standalone-amd64'.format(
|
||||
qutebrowser.__version__)
|
||||
@ -375,6 +365,8 @@ def pypi_upload(artifacts):
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--no-asciidoc', action='store_true',
|
||||
help="Don't generate docs")
|
||||
parser.add_argument('--asciidoc', help="Full path to python and "
|
||||
"asciidoc.py. If not given, it's searched in PATH.",
|
||||
nargs=2, required=False,
|
||||
@ -392,7 +384,11 @@ def main():
|
||||
import github3 # pylint: disable=unused-variable
|
||||
read_github_token()
|
||||
|
||||
run_asciidoc2html(args)
|
||||
if args.no_asciidoc:
|
||||
os.makedirs(os.path.join('qutebrowser', 'html', 'doc'), exist_ok=True)
|
||||
else:
|
||||
run_asciidoc2html(args)
|
||||
|
||||
if os.name == 'nt':
|
||||
artifacts = build_windows()
|
||||
elif sys.platform == 'darwin':
|
||||
|
@ -64,6 +64,8 @@ PERFECT_FILES = [
|
||||
'browser/webkit/cookies.py'),
|
||||
('tests/unit/browser/test_history.py',
|
||||
'browser/history.py'),
|
||||
('tests/unit/browser/test_pdfjs.py',
|
||||
'browser/pdfjs.py'),
|
||||
('tests/unit/browser/webkit/http/test_http.py',
|
||||
'browser/webkit/http.py'),
|
||||
('tests/unit/browser/webkit/http/test_content_disposition.py',
|
||||
@ -147,6 +149,8 @@ PERFECT_FILES = [
|
||||
'config/configcommands.py'),
|
||||
('tests/unit/config/test_configutils.py',
|
||||
'config/configutils.py'),
|
||||
('tests/unit/config/test_configcache.py',
|
||||
'config/configcache.py'),
|
||||
|
||||
('tests/unit/utils/test_qtutils.py',
|
||||
'utils/qtutils.py'),
|
||||
|
@ -65,9 +65,16 @@ def temp_basedir_env(tmpdir, short_tmpdir):
|
||||
runtime_dir.ensure(dir=True)
|
||||
runtime_dir.chmod(0o700)
|
||||
|
||||
(data_dir / 'qutebrowser' / 'state').write_text(
|
||||
'[general]\nquickstart-done = 1\nbackend-warning-shown=1',
|
||||
encoding='utf-8', ensure=True)
|
||||
lines = [
|
||||
'[general]',
|
||||
'quickstart-done = 1',
|
||||
'backend-warning-shown = 1',
|
||||
'old-qt-warning-shown = 1',
|
||||
'webkit-warning-shown = 1',
|
||||
]
|
||||
|
||||
state_file = data_dir / 'qutebrowser' / 'state'
|
||||
state_file.write_text('\n'.join(lines), encoding='utf-8', ensure=True)
|
||||
|
||||
env = {
|
||||
'XDG_DATA_HOME': str(data_dir),
|
||||
|
@ -42,9 +42,9 @@ from PyQt5.QtNetwork import QNetworkCookieJar
|
||||
import helpers.stubs as stubsmod
|
||||
import helpers.utils
|
||||
from qutebrowser.config import (config, configdata, configtypes, configexc,
|
||||
configfiles)
|
||||
configfiles, configcache)
|
||||
from qutebrowser.utils import objreg, standarddir, utils, usertypes
|
||||
from qutebrowser.browser import greasemonkey
|
||||
from qutebrowser.browser import greasemonkey, history
|
||||
from qutebrowser.browser.webkit import cookies
|
||||
from qutebrowser.misc import savemanager, sql, objects
|
||||
from qutebrowser.keyinput import modeman
|
||||
@ -253,6 +253,9 @@ def config_stub(stubs, monkeypatch, configdata_init, yaml_config_stub):
|
||||
container = config.ConfigContainer(conf)
|
||||
monkeypatch.setattr(config, 'val', container)
|
||||
|
||||
cache = configcache.ConfigCache()
|
||||
monkeypatch.setattr(config, 'cache', cache)
|
||||
|
||||
try:
|
||||
configtypes.Font.monospace_fonts = container.fonts.monospace
|
||||
except configexc.NoOptionError:
|
||||
@ -569,3 +572,14 @@ def download_stub(win_registry, tmpdir, stubs):
|
||||
objreg.register('qtnetwork-download-manager', stub)
|
||||
yield stub
|
||||
objreg.delete('qtnetwork-download-manager')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def web_history(fake_save_manager, tmpdir, init_sql, config_stub, stubs):
|
||||
"""Create a web history and register it into objreg."""
|
||||
config_stub.val.completion.timestamp_format = '%Y-%m-%d'
|
||||
config_stub.val.completion.web_history.max_items = -1
|
||||
web_history = history.WebHistory(stubs.FakeHistoryProgress())
|
||||
objreg.register('web-history', web_history)
|
||||
yield web_history
|
||||
objreg.delete('web-history')
|
||||
|
@ -632,3 +632,22 @@ class FakeDownloadManager:
|
||||
shutil.copyfileobj(fake_url_file, download_item.fileobj)
|
||||
self.downloads.append(download_item)
|
||||
return download_item
|
||||
|
||||
|
||||
class FakeHistoryProgress:
|
||||
|
||||
"""Fake for a WebHistoryProgress object."""
|
||||
|
||||
def __init__(self):
|
||||
self._started = False
|
||||
self._finished = False
|
||||
self._value = 0
|
||||
|
||||
def start(self, _text, _maximum):
|
||||
self._started = True
|
||||
|
||||
def tick(self):
|
||||
self._value += 1
|
||||
|
||||
def finish(self):
|
||||
self._finished = True
|
||||
|
@ -33,7 +33,7 @@ pytestmark = pytest.mark.usefixtures('qapp', 'config_tmpdir')
|
||||
|
||||
# TODO See ../utils/test_standarddirutils for OSError and caplog assertion
|
||||
|
||||
WHITELISTED_HOSTS = ('qutebrowser.org', 'mediumhost.io')
|
||||
WHITELISTED_HOSTS = ('qutebrowser.org', 'mediumhost.io', 'http://*.edu')
|
||||
|
||||
BLOCKLIST_HOSTS = ('localhost',
|
||||
'mediumhost.io',
|
||||
@ -50,7 +50,8 @@ URLS_TO_CHECK = ('http://localhost',
|
||||
'http://ads.worsthostever.net',
|
||||
'http://goodhost.gov',
|
||||
'ftp://verygoodhost.com',
|
||||
'http://qutebrowser.org')
|
||||
'http://qutebrowser.org',
|
||||
'http://veryverygoodhost.edu')
|
||||
|
||||
|
||||
class BaseDirStub:
|
||||
|
@ -37,110 +37,106 @@ def prerequisites(config_stub, fake_save_manager, init_sql, fake_args):
|
||||
config_stub.data = {'general': {'private-browsing': False}}
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def hist(tmpdir):
|
||||
return history.WebHistory()
|
||||
|
||||
|
||||
class TestSpecialMethods:
|
||||
|
||||
def test_iter(self, hist):
|
||||
def test_iter(self, web_history):
|
||||
urlstr = 'http://www.example.com/'
|
||||
url = QUrl(urlstr)
|
||||
hist.add_url(url, atime=12345)
|
||||
web_history.add_url(url, atime=12345)
|
||||
|
||||
assert list(hist) == [(urlstr, '', 12345, False)]
|
||||
assert list(web_history) == [(urlstr, '', 12345, False)]
|
||||
|
||||
def test_len(self, hist):
|
||||
assert len(hist) == 0
|
||||
def test_len(self, web_history):
|
||||
assert len(web_history) == 0
|
||||
|
||||
url = QUrl('http://www.example.com/')
|
||||
hist.add_url(url)
|
||||
web_history.add_url(url)
|
||||
|
||||
assert len(hist) == 1
|
||||
assert len(web_history) == 1
|
||||
|
||||
def test_contains(self, hist):
|
||||
hist.add_url(QUrl('http://www.example.com/'), title='Title',
|
||||
atime=12345)
|
||||
assert 'http://www.example.com/' in hist
|
||||
assert 'www.example.com' not in hist
|
||||
assert 'Title' not in hist
|
||||
assert 12345 not in hist
|
||||
def test_contains(self, web_history):
|
||||
web_history.add_url(QUrl('http://www.example.com/'),
|
||||
title='Title', atime=12345)
|
||||
assert 'http://www.example.com/' in web_history
|
||||
assert 'www.example.com' not in web_history
|
||||
assert 'Title' not in web_history
|
||||
assert 12345 not in web_history
|
||||
|
||||
|
||||
class TestGetting:
|
||||
|
||||
def test_get_recent(self, hist):
|
||||
hist.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890)
|
||||
hist.add_url(QUrl('http://example.com/'), atime=12345)
|
||||
assert list(hist.get_recent()) == [
|
||||
def test_get_recent(self, web_history):
|
||||
web_history.add_url(QUrl('http://www.qutebrowser.org/'), atime=67890)
|
||||
web_history.add_url(QUrl('http://example.com/'), atime=12345)
|
||||
assert list(web_history.get_recent()) == [
|
||||
('http://www.qutebrowser.org/', '', 67890, False),
|
||||
('http://example.com/', '', 12345, False),
|
||||
]
|
||||
|
||||
def test_entries_between(self, hist):
|
||||
hist.add_url(QUrl('http://www.example.com/1'), atime=12345)
|
||||
hist.add_url(QUrl('http://www.example.com/2'), atime=12346)
|
||||
hist.add_url(QUrl('http://www.example.com/3'), atime=12347)
|
||||
hist.add_url(QUrl('http://www.example.com/4'), atime=12348)
|
||||
hist.add_url(QUrl('http://www.example.com/5'), atime=12348)
|
||||
hist.add_url(QUrl('http://www.example.com/6'), atime=12349)
|
||||
hist.add_url(QUrl('http://www.example.com/7'), atime=12350)
|
||||
def test_entries_between(self, web_history):
|
||||
web_history.add_url(QUrl('http://www.example.com/1'), atime=12345)
|
||||
web_history.add_url(QUrl('http://www.example.com/2'), atime=12346)
|
||||
web_history.add_url(QUrl('http://www.example.com/3'), atime=12347)
|
||||
web_history.add_url(QUrl('http://www.example.com/4'), atime=12348)
|
||||
web_history.add_url(QUrl('http://www.example.com/5'), atime=12348)
|
||||
web_history.add_url(QUrl('http://www.example.com/6'), atime=12349)
|
||||
web_history.add_url(QUrl('http://www.example.com/7'), atime=12350)
|
||||
|
||||
times = [x.atime for x in hist.entries_between(12346, 12349)]
|
||||
times = [x.atime for x in web_history.entries_between(12346, 12349)]
|
||||
assert times == [12349, 12348, 12348, 12347]
|
||||
|
||||
def test_entries_before(self, hist):
|
||||
hist.add_url(QUrl('http://www.example.com/1'), atime=12346)
|
||||
hist.add_url(QUrl('http://www.example.com/2'), atime=12346)
|
||||
hist.add_url(QUrl('http://www.example.com/3'), atime=12347)
|
||||
hist.add_url(QUrl('http://www.example.com/4'), atime=12348)
|
||||
hist.add_url(QUrl('http://www.example.com/5'), atime=12348)
|
||||
hist.add_url(QUrl('http://www.example.com/6'), atime=12348)
|
||||
hist.add_url(QUrl('http://www.example.com/7'), atime=12349)
|
||||
hist.add_url(QUrl('http://www.example.com/8'), atime=12349)
|
||||
def test_entries_before(self, web_history):
|
||||
web_history.add_url(QUrl('http://www.example.com/1'), atime=12346)
|
||||
web_history.add_url(QUrl('http://www.example.com/2'), atime=12346)
|
||||
web_history.add_url(QUrl('http://www.example.com/3'), atime=12347)
|
||||
web_history.add_url(QUrl('http://www.example.com/4'), atime=12348)
|
||||
web_history.add_url(QUrl('http://www.example.com/5'), atime=12348)
|
||||
web_history.add_url(QUrl('http://www.example.com/6'), atime=12348)
|
||||
web_history.add_url(QUrl('http://www.example.com/7'), atime=12349)
|
||||
web_history.add_url(QUrl('http://www.example.com/8'), atime=12349)
|
||||
|
||||
times = [x.atime for x in
|
||||
hist.entries_before(12348, limit=3, offset=2)]
|
||||
web_history.entries_before(12348, limit=3, offset=2)]
|
||||
assert times == [12348, 12347, 12346]
|
||||
|
||||
|
||||
class TestDelete:
|
||||
|
||||
def test_clear(self, qtbot, tmpdir, hist, mocker):
|
||||
hist.add_url(QUrl('http://example.com/'))
|
||||
hist.add_url(QUrl('http://www.qutebrowser.org/'))
|
||||
def test_clear(self, qtbot, tmpdir, web_history, mocker):
|
||||
web_history.add_url(QUrl('http://example.com/'))
|
||||
web_history.add_url(QUrl('http://www.qutebrowser.org/'))
|
||||
|
||||
m = mocker.patch('qutebrowser.browser.history.message.confirm_async',
|
||||
new=mocker.Mock, spec=[])
|
||||
hist.clear()
|
||||
web_history.clear()
|
||||
assert m.called
|
||||
|
||||
def test_clear_force(self, qtbot, tmpdir, hist):
|
||||
hist.add_url(QUrl('http://example.com/'))
|
||||
hist.add_url(QUrl('http://www.qutebrowser.org/'))
|
||||
hist.clear(force=True)
|
||||
assert not len(hist)
|
||||
assert not len(hist.completion)
|
||||
def test_clear_force(self, qtbot, tmpdir, web_history):
|
||||
web_history.add_url(QUrl('http://example.com/'))
|
||||
web_history.add_url(QUrl('http://www.qutebrowser.org/'))
|
||||
web_history.clear(force=True)
|
||||
assert not len(web_history)
|
||||
assert not len(web_history.completion)
|
||||
|
||||
@pytest.mark.parametrize('raw, escaped', [
|
||||
('http://example.com/1', 'http://example.com/1'),
|
||||
('http://example.com/1 2', 'http://example.com/1%202'),
|
||||
])
|
||||
def test_delete_url(self, hist, raw, escaped):
|
||||
hist.add_url(QUrl('http://example.com/'), atime=0)
|
||||
hist.add_url(QUrl(escaped), atime=0)
|
||||
hist.add_url(QUrl('http://example.com/2'), atime=0)
|
||||
def test_delete_url(self, web_history, raw, escaped):
|
||||
web_history.add_url(QUrl('http://example.com/'), atime=0)
|
||||
web_history.add_url(QUrl(escaped), atime=0)
|
||||
web_history.add_url(QUrl('http://example.com/2'), atime=0)
|
||||
|
||||
before = set(hist)
|
||||
completion_before = set(hist.completion)
|
||||
before = set(web_history)
|
||||
completion_before = set(web_history.completion)
|
||||
|
||||
hist.delete_url(QUrl(raw))
|
||||
web_history.delete_url(QUrl(raw))
|
||||
|
||||
diff = before.difference(set(hist))
|
||||
diff = before.difference(set(web_history))
|
||||
assert diff == {(escaped, '', 0, False)}
|
||||
|
||||
completion_diff = completion_before.difference(set(hist.completion))
|
||||
completion_diff = completion_before.difference(
|
||||
set(web_history.completion))
|
||||
assert completion_diff == {(raw, '', 0)}
|
||||
|
||||
|
||||
@ -164,30 +160,32 @@ class TestAdd:
|
||||
'https://user@example.com', 'https://user@example.com'),
|
||||
]
|
||||
)
|
||||
def test_add_url(self, qtbot, hist,
|
||||
def test_add_url(self, qtbot, web_history,
|
||||
url, atime, title, redirect, history_url, completion_url):
|
||||
hist.add_url(QUrl(url), atime=atime, title=title, redirect=redirect)
|
||||
assert list(hist) == [(history_url, title, atime, redirect)]
|
||||
web_history.add_url(QUrl(url), atime=atime, title=title,
|
||||
redirect=redirect)
|
||||
assert list(web_history) == [(history_url, title, atime, redirect)]
|
||||
if completion_url is None:
|
||||
assert not len(hist.completion)
|
||||
assert not len(web_history.completion)
|
||||
else:
|
||||
assert list(hist.completion) == [(completion_url, title, atime)]
|
||||
expected = [(completion_url, title, atime)]
|
||||
assert list(web_history.completion) == expected
|
||||
|
||||
def test_no_sql_history(self, hist, fake_args):
|
||||
def test_no_sql_web_history(self, web_history, fake_args):
|
||||
fake_args.debug_flags = 'no-sql-history'
|
||||
hist.add_url(QUrl('https://www.example.com/'), atime=12346,
|
||||
title='Hello World', redirect=False)
|
||||
assert not list(hist)
|
||||
web_history.add_url(QUrl('https://www.example.com/'), atime=12346,
|
||||
title='Hello World', redirect=False)
|
||||
assert not list(web_history)
|
||||
|
||||
def test_invalid(self, qtbot, hist, caplog):
|
||||
def test_invalid(self, qtbot, web_history, caplog):
|
||||
with caplog.at_level(logging.WARNING):
|
||||
hist.add_url(QUrl())
|
||||
assert not list(hist)
|
||||
assert not list(hist.completion)
|
||||
web_history.add_url(QUrl())
|
||||
assert not list(web_history)
|
||||
assert not list(web_history.completion)
|
||||
|
||||
@pytest.mark.parametrize('environmental', [True, False])
|
||||
@pytest.mark.parametrize('completion', [True, False])
|
||||
def test_error(self, monkeypatch, hist, message_mock, caplog,
|
||||
def test_error(self, monkeypatch, web_history, message_mock, caplog,
|
||||
environmental, completion):
|
||||
def raise_error(url, replace=False):
|
||||
if environmental:
|
||||
@ -196,18 +194,18 @@ class TestAdd:
|
||||
raise sql.SqlBugError("Error message")
|
||||
|
||||
if completion:
|
||||
monkeypatch.setattr(hist.completion, 'insert', raise_error)
|
||||
monkeypatch.setattr(web_history.completion, 'insert', raise_error)
|
||||
else:
|
||||
monkeypatch.setattr(hist, 'insert', raise_error)
|
||||
monkeypatch.setattr(web_history, 'insert', raise_error)
|
||||
|
||||
if environmental:
|
||||
with caplog.at_level(logging.ERROR):
|
||||
hist.add_url(QUrl('https://www.example.org/'))
|
||||
web_history.add_url(QUrl('https://www.example.org/'))
|
||||
msg = message_mock.getmsg(usertypes.MessageLevel.error)
|
||||
assert msg.text == "Failed to write history: Error message"
|
||||
else:
|
||||
with pytest.raises(sql.SqlBugError):
|
||||
hist.add_url(QUrl('https://www.example.org/'))
|
||||
web_history.add_url(QUrl('https://www.example.org/'))
|
||||
|
||||
@pytest.mark.parametrize('level, url, req_url, expected', [
|
||||
(logging.DEBUG, 'a.com', 'a.com', [('a.com', 'title', 12345, False)]),
|
||||
@ -218,32 +216,33 @@ class TestAdd:
|
||||
(logging.WARNING, 'data:foo', '', []),
|
||||
(logging.WARNING, 'a.com', 'data:foo', []),
|
||||
])
|
||||
def test_from_tab(self, hist, caplog, mock_time,
|
||||
def test_from_tab(self, web_history, caplog, mock_time,
|
||||
level, url, req_url, expected):
|
||||
with caplog.at_level(level):
|
||||
hist.add_from_tab(QUrl(url), QUrl(req_url), 'title')
|
||||
assert set(hist) == set(expected)
|
||||
web_history.add_from_tab(QUrl(url), QUrl(req_url), 'title')
|
||||
assert set(web_history) == set(expected)
|
||||
|
||||
def test_exclude(self, hist, config_stub):
|
||||
def test_exclude(self, web_history, config_stub):
|
||||
"""Excluded URLs should be in the history but not completion."""
|
||||
config_stub.val.completion.web_history.exclude = ['*.example.org']
|
||||
url = QUrl('http://www.example.org/')
|
||||
hist.add_from_tab(url, url, 'title')
|
||||
assert list(hist)
|
||||
assert not list(hist.completion)
|
||||
web_history.add_from_tab(url, url, 'title')
|
||||
assert list(web_history)
|
||||
assert not list(web_history.completion)
|
||||
|
||||
|
||||
class TestHistoryInterface:
|
||||
|
||||
@pytest.fixture
|
||||
def hist_interface(self, hist):
|
||||
def hist_interface(self, web_history):
|
||||
# pylint: disable=invalid-name
|
||||
QtWebKit = pytest.importorskip('PyQt5.QtWebKit')
|
||||
from qutebrowser.browser.webkit import webkithistory
|
||||
QWebHistoryInterface = QtWebKit.QWebHistoryInterface
|
||||
# pylint: enable=invalid-name
|
||||
hist.add_url(url=QUrl('http://www.example.com/'), title='example')
|
||||
interface = webkithistory.WebHistoryInterface(hist)
|
||||
web_history.add_url(url=QUrl('http://www.example.com/'),
|
||||
title='example')
|
||||
interface = webkithistory.WebHistoryInterface(web_history)
|
||||
QWebHistoryInterface.setDefaultInterface(interface)
|
||||
yield
|
||||
QWebHistoryInterface.setDefaultInterface(None)
|
||||
@ -261,9 +260,9 @@ class TestInit:
|
||||
def cleanup_init(self):
|
||||
# prevent test_init from leaking state
|
||||
yield
|
||||
hist = objreg.get('web-history', None)
|
||||
if hist is not None:
|
||||
hist.setParent(None)
|
||||
web_history = objreg.get('web-history', None)
|
||||
if web_history is not None:
|
||||
web_history.setParent(None)
|
||||
objreg.delete('web-history')
|
||||
try:
|
||||
from PyQt5.QtWebKit import QWebHistoryInterface
|
||||
@ -304,202 +303,130 @@ class TestInit:
|
||||
assert default_interface is None
|
||||
|
||||
|
||||
class TestImport:
|
||||
|
||||
def test_import_txt(self, hist, data_tmpdir, monkeypatch, stubs):
|
||||
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
||||
histfile = data_tmpdir / 'history'
|
||||
# empty line is deliberate, to test skipping empty lines
|
||||
histfile.write('''12345 http://example.com/ title
|
||||
12346 http://qutebrowser.org/
|
||||
67890 http://example.com/path
|
||||
|
||||
68891-r http://example.com/path/other ''')
|
||||
|
||||
hist.import_txt()
|
||||
|
||||
assert list(hist) == [
|
||||
('http://example.com/', 'title', 12345, False),
|
||||
('http://qutebrowser.org/', '', 12346, False),
|
||||
('http://example.com/path', '', 67890, False),
|
||||
('http://example.com/path/other', '', 68891, True)
|
||||
]
|
||||
|
||||
assert not histfile.exists()
|
||||
assert (data_tmpdir / 'history.bak').exists()
|
||||
|
||||
def test_existing_backup(self, hist, data_tmpdir, monkeypatch, stubs):
|
||||
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
||||
histfile = data_tmpdir / 'history'
|
||||
bakfile = data_tmpdir / 'history.bak'
|
||||
histfile.write('12345 http://example.com/ title')
|
||||
bakfile.write('12346 http://qutebrowser.org/')
|
||||
|
||||
hist.import_txt()
|
||||
|
||||
assert list(hist) == [('http://example.com/', 'title', 12345, False)]
|
||||
|
||||
assert not histfile.exists()
|
||||
assert bakfile.read().split('\n') == ['12346 http://qutebrowser.org/',
|
||||
'12345 http://example.com/ title']
|
||||
|
||||
@pytest.mark.parametrize('line', [
|
||||
'',
|
||||
'#12345 http://example.com/commented',
|
||||
|
||||
# https://bugreports.qt.io/browse/QTBUG-60364
|
||||
'12345 http://.com/',
|
||||
'12345 https://.com/',
|
||||
'12345 http://www..com/',
|
||||
'12345 https://www..com/',
|
||||
|
||||
# issue #2646
|
||||
('12345 data:text/html;'
|
||||
'charset=UTF-8,%3C%21DOCTYPE%20html%20PUBLIC%20%22-'),
|
||||
])
|
||||
def test_skip(self, hist, data_tmpdir, monkeypatch, stubs, line):
|
||||
"""import_txt should skip certain lines silently."""
|
||||
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
||||
histfile = data_tmpdir / 'history'
|
||||
histfile.write(line)
|
||||
|
||||
hist.import_txt()
|
||||
|
||||
assert not histfile.exists()
|
||||
assert not len(hist)
|
||||
|
||||
@pytest.mark.parametrize('line', [
|
||||
'xyz http://example.com/bad-timestamp',
|
||||
'12345',
|
||||
'http://example.com/no-timestamp',
|
||||
'68891-r-r http://example.com/double-flag',
|
||||
'68891-x http://example.com/bad-flag',
|
||||
'68891 http://.com',
|
||||
])
|
||||
def test_invalid(self, hist, data_tmpdir, monkeypatch, stubs, caplog,
|
||||
line):
|
||||
"""import_txt should fail on certain lines."""
|
||||
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
||||
histfile = data_tmpdir / 'history'
|
||||
histfile.write(line)
|
||||
|
||||
with caplog.at_level(logging.ERROR):
|
||||
hist.import_txt()
|
||||
|
||||
assert any(rec.msg.startswith("Failed to import history:")
|
||||
for rec in caplog.records)
|
||||
|
||||
assert histfile.exists()
|
||||
|
||||
def test_nonexistent(self, hist, data_tmpdir, monkeypatch, stubs):
|
||||
"""import_txt should do nothing if the history file doesn't exist."""
|
||||
monkeypatch.setattr(history, 'QTimer', stubs.InstaTimer)
|
||||
hist.import_txt()
|
||||
|
||||
|
||||
class TestDump:
|
||||
|
||||
def test_debug_dump_history(self, hist, tmpdir):
|
||||
hist.add_url(QUrl('http://example.com/1'), title="Title1", atime=12345)
|
||||
hist.add_url(QUrl('http://example.com/2'), title="Title2", atime=12346)
|
||||
hist.add_url(QUrl('http://example.com/3'), title="Title3", atime=12347)
|
||||
hist.add_url(QUrl('http://example.com/4'), title="Title4", atime=12348,
|
||||
redirect=True)
|
||||
def test_debug_dump_history(self, web_history, tmpdir):
|
||||
web_history.add_url(QUrl('http://example.com/1'),
|
||||
title="Title1", atime=12345)
|
||||
web_history.add_url(QUrl('http://example.com/2'),
|
||||
title="Title2", atime=12346)
|
||||
web_history.add_url(QUrl('http://example.com/3'),
|
||||
title="Title3", atime=12347)
|
||||
web_history.add_url(QUrl('http://example.com/4'),
|
||||
title="Title4", atime=12348, redirect=True)
|
||||
histfile = tmpdir / 'history'
|
||||
hist.debug_dump_history(str(histfile))
|
||||
web_history.debug_dump_history(str(histfile))
|
||||
expected = ['12345 http://example.com/1 Title1',
|
||||
'12346 http://example.com/2 Title2',
|
||||
'12347 http://example.com/3 Title3',
|
||||
'12348-r http://example.com/4 Title4']
|
||||
assert histfile.read() == '\n'.join(expected)
|
||||
|
||||
def test_nonexistent(self, hist, tmpdir):
|
||||
def test_nonexistent(self, web_history, tmpdir):
|
||||
histfile = tmpdir / 'nonexistent' / 'history'
|
||||
with pytest.raises(cmdexc.CommandError):
|
||||
hist.debug_dump_history(str(histfile))
|
||||
web_history.debug_dump_history(str(histfile))
|
||||
|
||||
|
||||
class TestRebuild:
|
||||
|
||||
def test_delete(self, hist):
|
||||
hist.insert({'url': 'example.com/1', 'title': 'example1',
|
||||
'redirect': False, 'atime': 1})
|
||||
hist.insert({'url': 'example.com/1', 'title': 'example1',
|
||||
'redirect': False, 'atime': 2})
|
||||
hist.insert({'url': 'example.com/2%203', 'title': 'example2',
|
||||
'redirect': False, 'atime': 3})
|
||||
hist.insert({'url': 'example.com/3', 'title': 'example3',
|
||||
'redirect': True, 'atime': 4})
|
||||
hist.insert({'url': 'example.com/2 3', 'title': 'example2',
|
||||
'redirect': False, 'atime': 5})
|
||||
hist.completion.delete_all()
|
||||
def test_delete(self, web_history, stubs):
|
||||
web_history.insert({'url': 'example.com/1', 'title': 'example1',
|
||||
'redirect': False, 'atime': 1})
|
||||
web_history.insert({'url': 'example.com/1', 'title': 'example1',
|
||||
'redirect': False, 'atime': 2})
|
||||
web_history.insert({'url': 'example.com/2%203', 'title': 'example2',
|
||||
'redirect': False, 'atime': 3})
|
||||
web_history.insert({'url': 'example.com/3', 'title': 'example3',
|
||||
'redirect': True, 'atime': 4})
|
||||
web_history.insert({'url': 'example.com/2 3', 'title': 'example2',
|
||||
'redirect': False, 'atime': 5})
|
||||
web_history.completion.delete_all()
|
||||
|
||||
hist2 = history.WebHistory()
|
||||
hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress())
|
||||
assert list(hist2.completion) == [
|
||||
('example.com/1', 'example1', 2),
|
||||
('example.com/2 3', 'example2', 5),
|
||||
]
|
||||
|
||||
def test_no_rebuild(self, hist):
|
||||
def test_no_rebuild(self, web_history, stubs):
|
||||
"""Ensure that completion is not regenerated unless empty."""
|
||||
hist.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
||||
hist.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
||||
hist.completion.delete('url', 'example.com/2')
|
||||
web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
||||
web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
||||
web_history.completion.delete('url', 'example.com/2')
|
||||
|
||||
hist2 = history.WebHistory()
|
||||
hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress())
|
||||
assert list(hist2.completion) == [('example.com/1', '', 1)]
|
||||
|
||||
def test_user_version(self, hist, monkeypatch):
|
||||
def test_user_version(self, web_history, stubs, monkeypatch):
|
||||
"""Ensure that completion is regenerated if user_version changes."""
|
||||
hist.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
||||
hist.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
||||
hist.completion.delete('url', 'example.com/2')
|
||||
web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
||||
web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
||||
web_history.completion.delete('url', 'example.com/2')
|
||||
|
||||
hist2 = history.WebHistory()
|
||||
hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress())
|
||||
assert list(hist2.completion) == [('example.com/1', '', 1)]
|
||||
|
||||
monkeypatch.setattr(history, '_USER_VERSION',
|
||||
history._USER_VERSION + 1)
|
||||
hist3 = history.WebHistory()
|
||||
hist3 = history.WebHistory(progress=stubs.FakeHistoryProgress())
|
||||
assert list(hist3.completion) == [
|
||||
('example.com/1', '', 1),
|
||||
('example.com/2', '', 2),
|
||||
]
|
||||
|
||||
def test_force_rebuild(self, hist):
|
||||
def test_force_rebuild(self, web_history, stubs):
|
||||
"""Ensure that completion is regenerated if we force a rebuild."""
|
||||
hist.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
||||
hist.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
||||
hist.completion.delete('url', 'example.com/2')
|
||||
web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
||||
web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
||||
web_history.completion.delete('url', 'example.com/2')
|
||||
|
||||
hist2 = history.WebHistory()
|
||||
hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress())
|
||||
assert list(hist2.completion) == [('example.com/1', '', 1)]
|
||||
hist2.metainfo['force_rebuild'] = True
|
||||
|
||||
hist3 = history.WebHistory()
|
||||
hist3 = history.WebHistory(progress=stubs.FakeHistoryProgress())
|
||||
assert list(hist3.completion) == [
|
||||
('example.com/1', '', 1),
|
||||
('example.com/2', '', 2),
|
||||
]
|
||||
assert not hist3.metainfo['force_rebuild']
|
||||
|
||||
def test_exclude(self, config_stub, hist):
|
||||
def test_exclude(self, config_stub, web_history, stubs):
|
||||
"""Ensure that patterns in completion.web_history.exclude are ignored.
|
||||
|
||||
This setting should only be used for the completion.
|
||||
"""
|
||||
config_stub.val.completion.web_history.exclude = ['*.example.org']
|
||||
assert hist.metainfo['force_rebuild']
|
||||
assert web_history.metainfo['force_rebuild']
|
||||
|
||||
hist.add_url(QUrl('http://example.com'), redirect=False, atime=1)
|
||||
hist.add_url(QUrl('http://example.org'), redirect=False, atime=2)
|
||||
web_history.add_url(QUrl('http://example.com'),
|
||||
redirect=False, atime=1)
|
||||
web_history.add_url(QUrl('http://example.org'),
|
||||
redirect=False, atime=2)
|
||||
|
||||
hist2 = history.WebHistory()
|
||||
hist2 = history.WebHistory(progress=stubs.FakeHistoryProgress())
|
||||
assert list(hist2.completion) == [('http://example.com', '', 1)]
|
||||
|
||||
def test_unrelated_config_change(self, config_stub, hist):
|
||||
def test_unrelated_config_change(self, config_stub, web_history):
|
||||
config_stub.val.history_gap_interval = 1234
|
||||
assert not hist.metainfo['force_rebuild']
|
||||
assert not web_history.metainfo['force_rebuild']
|
||||
|
||||
@pytest.mark.parametrize('patch_threshold', [True, False])
|
||||
def test_progress(self, web_history, config_stub, monkeypatch, stubs,
|
||||
patch_threshold):
|
||||
web_history.add_url(QUrl('example.com/1'), redirect=False, atime=1)
|
||||
web_history.add_url(QUrl('example.com/2'), redirect=False, atime=2)
|
||||
web_history.metainfo['force_rebuild'] = True
|
||||
|
||||
if patch_threshold:
|
||||
monkeypatch.setattr(history.WebHistory, '_PROGRESS_THRESHOLD', 1)
|
||||
|
||||
progress = stubs.FakeHistoryProgress()
|
||||
history.WebHistory(progress=progress)
|
||||
assert progress._value == 2
|
||||
assert progress._finished
|
||||
assert progress._started == patch_threshold
|
||||
|
||||
|
||||
class TestCompletionMetaInfo:
|
||||
@ -527,3 +454,33 @@ class TestCompletionMetaInfo:
|
||||
assert not metainfo['force_rebuild']
|
||||
metainfo['force_rebuild'] = True
|
||||
assert metainfo['force_rebuild']
|
||||
|
||||
|
||||
class TestHistoryProgress:
|
||||
|
||||
@pytest.fixture
|
||||
def progress(self):
|
||||
return history.HistoryProgress()
|
||||
|
||||
def test_no_start(self, progress):
|
||||
"""Test calling tick/finish without start."""
|
||||
progress.tick()
|
||||
progress.finish()
|
||||
assert progress._progress is None
|
||||
assert progress._value == 1
|
||||
|
||||
def test_gui(self, qtbot, progress):
|
||||
progress.start("Hello World", 42)
|
||||
dialog = progress._progress
|
||||
qtbot.add_widget(dialog)
|
||||
progress.tick()
|
||||
|
||||
assert dialog.isVisible()
|
||||
assert dialog.labelText() == "Hello World"
|
||||
assert dialog.minimum() == 0
|
||||
assert dialog.maximum() == 42
|
||||
assert dialog.value() == 1
|
||||
assert dialog.minimumDuration() == 500
|
||||
|
||||
progress.finish()
|
||||
assert not dialog.isVisible()
|
||||
|
@ -17,51 +17,117 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.browser import pdfjs
|
||||
from qutebrowser.utils import usertypes, utils
|
||||
|
||||
|
||||
@pytest.mark.parametrize('available, snippet', [
|
||||
pytest.param(True, '<title>PDF.js viewer</title>',
|
||||
marks=pytest.mark.skipif(not pdfjs.is_available(),
|
||||
reason='PDF.js unavailable')),
|
||||
(False, '<h1>No pdf.js installation found</h1>'),
|
||||
('force', 'fake PDF.js'),
|
||||
])
|
||||
def test_generate_pdfjs_page(available, snippet, monkeypatch):
|
||||
if available == 'force':
|
||||
monkeypatch.setattr(pdfjs, 'is_available', lambda: True)
|
||||
monkeypatch.setattr(pdfjs, 'get_pdfjs_res',
|
||||
lambda filename: b'fake PDF.js')
|
||||
else:
|
||||
monkeypatch.setattr(pdfjs, 'is_available', lambda: available)
|
||||
|
||||
content = pdfjs.generate_pdfjs_page('example.pdf', QUrl())
|
||||
print(content)
|
||||
assert snippet in content
|
||||
|
||||
|
||||
# Note that we got double protection, once because we use QUrl.FullyEncoded and
|
||||
# because we use qutebrowser.utils.javascript.string_escape. Characters
|
||||
# like " are already replaced by QUrl.
|
||||
@pytest.mark.parametrize('url, expected', [
|
||||
('http://foo.bar', "http://foo.bar"),
|
||||
('http://"', ''),
|
||||
('\0', '%00'),
|
||||
('http://foobar/");alert("attack!");',
|
||||
'http://foobar/%22);alert(%22attack!%22);'),
|
||||
@pytest.mark.parametrize('filename, expected', [
|
||||
('foo.bar', "foo.bar"),
|
||||
('foo"bar', "foo%22bar"),
|
||||
('foo\0bar', 'foo%00bar'),
|
||||
('foobar");alert("attack!");',
|
||||
'foobar%22);alert(%22attack!%22);'),
|
||||
])
|
||||
def test_generate_pdfjs_script(url, expected):
|
||||
expected_open = 'open("{}");'.format(expected)
|
||||
url = QUrl(url)
|
||||
actual = pdfjs._generate_pdfjs_script(url)
|
||||
def test_generate_pdfjs_script(filename, expected):
|
||||
expected_open = 'open("qute://pdfjs/file?filename={}");'.format(expected)
|
||||
actual = pdfjs._generate_pdfjs_script(filename)
|
||||
assert expected_open in actual
|
||||
assert 'PDFView' in actual
|
||||
|
||||
|
||||
def test_fix_urls():
|
||||
page = textwrap.dedent("""
|
||||
<html>
|
||||
<script src="viewer.js"></script>
|
||||
<link href="viewer.css">
|
||||
<script src="unrelated.js"></script>
|
||||
</html>
|
||||
""").strip()
|
||||
@pytest.mark.parametrize('qt, backend, expected', [
|
||||
('new', usertypes.Backend.QtWebEngine, False),
|
||||
('new', usertypes.Backend.QtWebKit, False),
|
||||
('old', usertypes.Backend.QtWebEngine, True),
|
||||
('old', usertypes.Backend.QtWebKit, False),
|
||||
('5.7', usertypes.Backend.QtWebEngine, False),
|
||||
('5.7', usertypes.Backend.QtWebKit, False),
|
||||
])
|
||||
def test_generate_pdfjs_script_disable_object_url(monkeypatch,
|
||||
qt, backend, expected):
|
||||
if qt == 'new':
|
||||
monkeypatch.setattr(pdfjs.qtutils, 'version_check',
|
||||
lambda version, exact=False, compiled=True:
|
||||
False if version == '5.7.1' else True)
|
||||
elif qt == 'old':
|
||||
monkeypatch.setattr(pdfjs.qtutils, 'version_check',
|
||||
lambda version, exact=False, compiled=True: False)
|
||||
elif qt == '5.7':
|
||||
monkeypatch.setattr(pdfjs.qtutils, 'version_check',
|
||||
lambda version, exact=False, compiled=True:
|
||||
True if version == '5.7.1' else False)
|
||||
else:
|
||||
raise utils.Unreachable
|
||||
|
||||
expected = textwrap.dedent("""
|
||||
<html>
|
||||
<script src="qute://pdfjs/web/viewer.js"></script>
|
||||
<link href="qute://pdfjs/web/viewer.css">
|
||||
<script src="unrelated.js"></script>
|
||||
</html>
|
||||
""").strip()
|
||||
monkeypatch.setattr(pdfjs.objects, 'backend', backend)
|
||||
|
||||
actual = pdfjs.fix_urls(page)
|
||||
assert actual == expected
|
||||
script = pdfjs._generate_pdfjs_script('testfile')
|
||||
assert ('PDFJS.disableCreateObjectURL' in script) == expected
|
||||
|
||||
|
||||
class TestResources:
|
||||
|
||||
@pytest.fixture
|
||||
def read_system_mock(self, mocker):
|
||||
return mocker.patch.object(pdfjs, '_read_from_system', autospec=True)
|
||||
|
||||
@pytest.fixture
|
||||
def read_file_mock(self, mocker):
|
||||
return mocker.patch.object(pdfjs.utils, 'read_file', autospec=True)
|
||||
|
||||
def test_get_pdfjs_res_system(self, read_system_mock):
|
||||
read_system_mock.return_value = (b'content', 'path')
|
||||
|
||||
assert pdfjs.get_pdfjs_res_and_path('web/test') == (b'content', 'path')
|
||||
assert pdfjs.get_pdfjs_res('web/test') == b'content'
|
||||
|
||||
read_system_mock.assert_called_with('/usr/share/pdf.js/',
|
||||
['web/test', 'test'])
|
||||
|
||||
def test_get_pdfjs_res_bundled(self, read_system_mock, read_file_mock):
|
||||
read_system_mock.return_value = (None, None)
|
||||
|
||||
read_file_mock.return_value = b'content'
|
||||
|
||||
assert pdfjs.get_pdfjs_res_and_path('web/test') == (b'content', None)
|
||||
assert pdfjs.get_pdfjs_res('web/test') == b'content'
|
||||
|
||||
for path in pdfjs.SYSTEM_PDFJS_PATHS:
|
||||
read_system_mock.assert_any_call(path, ['web/test', 'test'])
|
||||
|
||||
def test_get_pdfjs_res_not_found(self, read_system_mock, read_file_mock):
|
||||
read_system_mock.return_value = (None, None)
|
||||
read_file_mock.side_effect = FileNotFoundError
|
||||
|
||||
with pytest.raises(pdfjs.PDFJSNotFound,
|
||||
match="Path 'web/test' not found"):
|
||||
pdfjs.get_pdfjs_res_and_path('web/test')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('path, expected', [
|
||||
@ -72,3 +138,58 @@ def test_fix_urls():
|
||||
])
|
||||
def test_remove_prefix(path, expected):
|
||||
assert pdfjs._remove_prefix(path) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('names, expected_name', [
|
||||
(['one'], 'one'),
|
||||
(['doesnotexist', 'two'], 'two'),
|
||||
(['one', 'two'], 'one'),
|
||||
(['does', 'not', 'onexist'], None),
|
||||
])
|
||||
def test_read_from_system(names, expected_name, tmpdir):
|
||||
file1 = tmpdir / 'one'
|
||||
file1.write_text('text1', encoding='ascii')
|
||||
file2 = tmpdir / 'two'
|
||||
file2.write_text('text2', encoding='ascii')
|
||||
|
||||
if expected_name == 'one':
|
||||
expected = (b'text1', str(file1))
|
||||
elif expected_name == 'two':
|
||||
expected = (b'text2', str(file2))
|
||||
elif expected_name is None:
|
||||
expected = (None, None)
|
||||
|
||||
assert pdfjs._read_from_system(str(tmpdir), names) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('available', [True, False])
|
||||
def test_is_available(available, mocker):
|
||||
mock = mocker.patch.object(pdfjs, 'get_pdfjs_res', autospec=True)
|
||||
if available:
|
||||
mock.return_value = b'foo'
|
||||
else:
|
||||
mock.side_effect = pdfjs.PDFJSNotFound('build/pdf.js')
|
||||
|
||||
assert pdfjs.is_available() == available
|
||||
|
||||
|
||||
@pytest.mark.parametrize('mimetype, url, enabled, expected', [
|
||||
# PDF files
|
||||
('application/pdf', 'http://www.example.com', True, True),
|
||||
('application/x-pdf', 'http://www.example.com', True, True),
|
||||
# Not a PDF
|
||||
('application/octet-stream', 'http://www.example.com', True, False),
|
||||
# PDF.js disabled
|
||||
('application/pdf', 'http://www.example.com', False, False),
|
||||
# Download button in PDF.js
|
||||
('application/pdf', 'blob:qute%3A///b45250b3', True, False),
|
||||
])
|
||||
def test_should_use_pdfjs(mimetype, url, enabled, expected, config_stub):
|
||||
config_stub.val.content.pdfjs = enabled
|
||||
assert pdfjs.should_use_pdfjs(mimetype, QUrl(url)) == expected
|
||||
|
||||
|
||||
def test_get_main_url():
|
||||
expected = ('qute://pdfjs/web/viewer.html?filename='
|
||||
'hello?world.pdf&file=')
|
||||
assert pdfjs.get_main_url('hello?world.pdf') == QUrl(expected)
|
||||
|
@ -20,12 +20,13 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
import py.path # pylint: disable=no-name-in-module
|
||||
from PyQt5.QtCore import QUrl, QUrlQuery
|
||||
import pytest
|
||||
|
||||
from qutebrowser.browser import history, qutescheme
|
||||
from qutebrowser.utils import objreg
|
||||
from qutebrowser.browser import qutescheme, pdfjs, downloads
|
||||
|
||||
|
||||
class TestJavascriptHandler:
|
||||
@ -96,21 +97,12 @@ class TestHistoryHandler:
|
||||
|
||||
return items
|
||||
|
||||
@pytest.fixture
|
||||
def fake_web_history(self, fake_save_manager, tmpdir, init_sql,
|
||||
config_stub):
|
||||
"""Create a fake web-history and register it into objreg."""
|
||||
web_history = history.WebHistory()
|
||||
objreg.register('web-history', web_history)
|
||||
yield web_history
|
||||
objreg.delete('web-history')
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_history(self, fake_web_history, fake_args, entries):
|
||||
def fake_history(self, web_history, fake_args, entries):
|
||||
"""Create fake history."""
|
||||
fake_args.debug_flags = []
|
||||
for item in entries:
|
||||
fake_web_history.add_url(**item)
|
||||
web_history.add_url(**item)
|
||||
|
||||
@pytest.mark.parametrize("start_time_offset, expected_item_count", [
|
||||
(0, 4),
|
||||
@ -134,7 +126,7 @@ class TestHistoryHandler:
|
||||
assert item['time'] <= start_time
|
||||
assert item['time'] > end_time
|
||||
|
||||
def test_exclude(self, fake_web_history, now, config_stub):
|
||||
def test_exclude(self, web_history, now, config_stub):
|
||||
"""Make sure the completion.web_history.exclude setting is not used."""
|
||||
config_stub.val.completion.web_history.exclude = ['www.x.com']
|
||||
|
||||
@ -143,7 +135,7 @@ class TestHistoryHandler:
|
||||
items = json.loads(data)
|
||||
assert items
|
||||
|
||||
def test_qute_history_benchmark(self, fake_web_history, benchmark, now):
|
||||
def test_qute_history_benchmark(self, web_history, benchmark, now):
|
||||
r = range(100000)
|
||||
entries = {
|
||||
'atime': [int(now - t) for t in r],
|
||||
@ -152,7 +144,7 @@ class TestHistoryHandler:
|
||||
'redirect': [False for _ in r],
|
||||
}
|
||||
|
||||
fake_web_history.insert_batch(entries)
|
||||
web_history.insert_batch(entries)
|
||||
url = QUrl("qute://history/data?start_time={}".format(now))
|
||||
_mimetype, data = benchmark(qutescheme.qute_history, url)
|
||||
assert len(json.loads(data)) > 1
|
||||
@ -179,3 +171,68 @@ class TestHelpHandler:
|
||||
mimetype, data = qutescheme.qute_help(QUrl('qute://help/foo.bin'))
|
||||
assert mimetype == 'application/octet-stream'
|
||||
assert data == b'\xff'
|
||||
|
||||
|
||||
class TestPDFJSHandler:
|
||||
|
||||
"""Test the qute://pdfjs endpoint."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_pdfjs(self, monkeypatch):
|
||||
def get_pdfjs_res(path):
|
||||
if path == '/existing/file.html':
|
||||
return b'foobar'
|
||||
raise pdfjs.PDFJSNotFound(path)
|
||||
|
||||
monkeypatch.setattr(pdfjs, 'get_pdfjs_res', get_pdfjs_res)
|
||||
|
||||
@pytest.fixture
|
||||
def download_tmpdir(self):
|
||||
tdir = downloads.temp_download_manager.get_tmpdir()
|
||||
yield py.path.local(tdir.name) # pylint: disable=no-member
|
||||
tdir.cleanup()
|
||||
|
||||
def test_existing_resource(self):
|
||||
"""Test with a resource that exists."""
|
||||
_mimetype, data = qutescheme.data_for_url(
|
||||
QUrl('qute://pdfjs/existing/file.html'))
|
||||
assert data == b'foobar'
|
||||
|
||||
def test_nonexisting_resource(self, caplog):
|
||||
"""Test with a resource that does not exist."""
|
||||
with caplog.at_level(logging.WARNING, 'misc'):
|
||||
with pytest.raises(qutescheme.NotFoundError):
|
||||
qutescheme.data_for_url(QUrl('qute://pdfjs/no/file.html'))
|
||||
assert len(caplog.records) == 1
|
||||
assert (caplog.records[0].message ==
|
||||
'pdfjs resource requested but not found: /no/file.html')
|
||||
|
||||
def test_viewer_page(self):
|
||||
"""Load the /web/viewer.html page."""
|
||||
_mimetype, data = qutescheme.data_for_url(
|
||||
QUrl('qute://pdfjs/web/viewer.html?filename=foobar'))
|
||||
assert b'PDF.js' in data
|
||||
|
||||
def test_viewer_no_filename(self):
|
||||
with pytest.raises(qutescheme.UrlInvalidError):
|
||||
qutescheme.data_for_url(QUrl('qute://pdfjs/web/viewer.html'))
|
||||
|
||||
def test_file(self, download_tmpdir):
|
||||
"""Load a file via qute://pdfjs/file."""
|
||||
(download_tmpdir / 'testfile').write_binary(b'foo')
|
||||
_mimetype, data = qutescheme.data_for_url(
|
||||
QUrl('qute://pdfjs/file?filename=testfile'))
|
||||
assert data == b'foo'
|
||||
|
||||
def test_file_no_filename(self):
|
||||
with pytest.raises(qutescheme.UrlInvalidError):
|
||||
qutescheme.data_for_url(QUrl('qute://pdfjs/file'))
|
||||
|
||||
@pytest.mark.parametrize('sep', ['/', os.sep])
|
||||
def test_file_pathsep(self, sep):
|
||||
url = QUrl('qute://pdfjs/file')
|
||||
query = QUrlQuery()
|
||||
query.addQueryItem('filename', 'foo{}bar'.format(sep))
|
||||
url.setQuery(query)
|
||||
with pytest.raises(qutescheme.RequestDeniedError):
|
||||
qutescheme.data_for_url(url)
|
||||
|
@ -1,63 +0,0 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2016-2018 Daniel Schadt
|
||||
# Copyright 2016-2018 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.browser import pdfjs, qutescheme
|
||||
# pylint: disable=unused-import
|
||||
from qutebrowser.browser.webkit.network import webkitqutescheme
|
||||
# pylint: enable=unused-import
|
||||
|
||||
|
||||
class TestPDFJSHandler:
|
||||
"""Test the qute://pdfjs endpoint."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def fake_pdfjs(self, monkeypatch):
|
||||
def get_pdfjs_res(path):
|
||||
if path == '/existing/file.html':
|
||||
return b'foobar'
|
||||
raise pdfjs.PDFJSNotFound(path)
|
||||
|
||||
monkeypatch.setattr(pdfjs, 'get_pdfjs_res', get_pdfjs_res)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_backend(self, monkeypatch):
|
||||
monkeypatch.setattr(qutescheme.objects, 'backend',
|
||||
usertypes.Backend.QtWebKit)
|
||||
|
||||
def test_existing_resource(self):
|
||||
"""Test with a resource that exists."""
|
||||
_mimetype, data = qutescheme.data_for_url(
|
||||
QUrl('qute://pdfjs/existing/file.html'))
|
||||
assert data == b'foobar'
|
||||
|
||||
def test_nonexisting_resource(self, caplog):
|
||||
"""Test with a resource that does not exist."""
|
||||
with caplog.at_level(logging.WARNING, 'misc'):
|
||||
with pytest.raises(qutescheme.NotFoundError):
|
||||
qutescheme.data_for_url(QUrl('qute://pdfjs/no/file.html'))
|
||||
assert len(caplog.records) == 1
|
||||
assert (caplog.records[0].message ==
|
||||
'pdfjs resource requested but not found: /no/file.html')
|
@ -68,10 +68,6 @@ def test_page_titles(url, title, out):
|
||||
|
||||
class TestDownloadTarget:
|
||||
|
||||
def test_base(self):
|
||||
with pytest.raises(NotImplementedError):
|
||||
downloads._DownloadTarget()
|
||||
|
||||
def test_filename(self):
|
||||
target = downloads.FileDownloadTarget("/foo/bar")
|
||||
assert target.filename == "/foo/bar"
|
||||
|
@ -30,8 +30,7 @@ from PyQt5.QtCore import QUrl
|
||||
from qutebrowser.completion import completer
|
||||
from qutebrowser.completion.models import miscmodels, urlmodel, configmodel
|
||||
from qutebrowser.config import configdata, configtypes
|
||||
from qutebrowser.utils import objreg, usertypes
|
||||
from qutebrowser.browser import history
|
||||
from qutebrowser.utils import usertypes
|
||||
from qutebrowser.commands import cmdutils
|
||||
|
||||
|
||||
@ -168,17 +167,6 @@ def bookmarks(bookmark_manager_stub):
|
||||
return bookmark_manager_stub
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def web_history(init_sql, stubs, config_stub):
|
||||
"""Fixture which provides a web-history object."""
|
||||
config_stub.val.completion.timestamp_format = '%Y-%m-%d'
|
||||
config_stub.val.completion.web_history.max_items = -1
|
||||
stub = history.WebHistory()
|
||||
objreg.register('web-history', stub)
|
||||
yield stub
|
||||
objreg.delete('web-history')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def web_history_populated(web_history):
|
||||
"""Pre-populate the web-history database."""
|
||||
|
52
tests/unit/config/test_configcache.py
Normal file
52
tests/unit/config/test_configcache.py
Normal file
@ -0,0 +1,52 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2018 Jay Kamat <jaygkamat@gmail.com>:
|
||||
#
|
||||
# This file is part of qutebrowser.
|
||||
#
|
||||
# qutebrowser is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# qutebrowser is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with qutebrowser. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# False-positives
|
||||
# FIXME: Report this to pylint?
|
||||
# pylint: disable=unsubscriptable-object
|
||||
|
||||
"""Tests for qutebrowser.config.configcache."""
|
||||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.config import config
|
||||
|
||||
|
||||
def test_configcache_except_pattern(config_stub):
|
||||
with pytest.raises(AssertionError):
|
||||
assert config.cache['content.javascript.enabled']
|
||||
|
||||
|
||||
def test_configcache_error_set(config_stub):
|
||||
# pylint: disable=unsupported-assignment-operation
|
||||
with pytest.raises(TypeError):
|
||||
config.cache['content.javascript.enabled'] = True
|
||||
|
||||
|
||||
def test_configcache_get(config_stub):
|
||||
assert len(config.cache._cache) == 0
|
||||
assert not config.cache['auto_save.session']
|
||||
assert len(config.cache._cache) == 1
|
||||
assert not config.cache['auto_save.session']
|
||||
|
||||
|
||||
def test_configcache_get_after_set(config_stub):
|
||||
assert not config.cache['auto_save.session']
|
||||
config_stub.val.auto_save.session = True
|
||||
assert config.cache['auto_save.session']
|
@ -235,6 +235,21 @@ class TestYaml:
|
||||
data = autoconfig.read()
|
||||
assert 'bindings.default' not in data
|
||||
|
||||
@pytest.mark.parametrize('public_only, expected', [
|
||||
(True, 'default-public-interface-only'),
|
||||
(False, 'all-interfaces'),
|
||||
])
|
||||
def test_webrtc(self, yaml, autoconfig, public_only, expected):
|
||||
"""Tests for migration of content.webrtc_public_interfaces_only."""
|
||||
autoconfig.write({'content.webrtc_public_interfaces_only':
|
||||
{'global': public_only}})
|
||||
|
||||
yaml.load()
|
||||
yaml._save()
|
||||
|
||||
data = autoconfig.read()
|
||||
assert data['content.webrtc_ip_handling_policy']['global'] == expected
|
||||
|
||||
@pytest.mark.parametrize('show, expected', [
|
||||
(True, 'always'),
|
||||
(False, 'never'),
|
||||
|
@ -359,10 +359,11 @@ class TestQtArgs:
|
||||
return parser
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_version_check(self, monkeypatch):
|
||||
"""Make sure no --disable-shared-workers argument gets added."""
|
||||
def reduce_args(self, monkeypatch, config_stub):
|
||||
"""Make sure no --disable-shared-workers/referer argument get added."""
|
||||
monkeypatch.setattr(configinit.qtutils, 'version_check',
|
||||
lambda version, compiled=False: True)
|
||||
config_stub.val.content.headers.referer = 'always'
|
||||
|
||||
@pytest.mark.parametrize('args, expected', [
|
||||
# No Qt arguments
|
||||
@ -438,23 +439,35 @@ class TestQtArgs:
|
||||
assert ('--autoplay-policy=user-gesture-required' in args) == added
|
||||
|
||||
@utils.qt59
|
||||
@pytest.mark.parametrize('new_version, public_only, added', [
|
||||
(True, True, False), # new enough to not need it
|
||||
(False, False, False), # option disabled
|
||||
(False, True, True),
|
||||
@pytest.mark.parametrize('policy, arg', [
|
||||
('all-interfaces', None),
|
||||
|
||||
('default-public-and-private-interfaces',
|
||||
'--force-webrtc-ip-handling-policy='
|
||||
'default_public_and_private_interfaces'),
|
||||
|
||||
('default-public-interface-only',
|
||||
'--force-webrtc-ip-handling-policy='
|
||||
'default_public_interface_only'),
|
||||
|
||||
('disable-non-proxied-udp',
|
||||
'--force-webrtc-ip-handling-policy='
|
||||
'disable_non_proxied_udp'),
|
||||
])
|
||||
def test_webrtc(self, config_stub, monkeypatch, parser,
|
||||
new_version, public_only, added):
|
||||
policy, arg):
|
||||
monkeypatch.setattr(configinit.objects, 'backend',
|
||||
usertypes.Backend.QtWebEngine)
|
||||
config_stub.val.content.webrtc_public_interfaces_only = public_only
|
||||
monkeypatch.setattr(configinit.qtutils, 'version_check',
|
||||
lambda version, compiled=False: new_version)
|
||||
config_stub.val.content.webrtc_ip_handling_policy = policy
|
||||
|
||||
parsed = parser.parse_args([])
|
||||
args = configinit.qt_args(parsed)
|
||||
arg = '--force-webrtc-ip-handling-policy=default_public_interface_only'
|
||||
assert (arg in args) == added
|
||||
|
||||
if arg is None:
|
||||
assert not any(a.startswith('--force-webrtc-ip-handling-policy=')
|
||||
for a in args)
|
||||
else:
|
||||
assert arg in args
|
||||
|
||||
@pytest.mark.parametrize('canvas_reading, added', [
|
||||
(True, False), # canvas reading enabled
|
||||
@ -470,6 +483,67 @@ class TestQtArgs:
|
||||
args = configinit.qt_args(parsed)
|
||||
assert ('--disable-reading-from-canvas' in args) == added
|
||||
|
||||
@pytest.mark.parametrize('process_model, added', [
|
||||
('process-per-site-instance', False),
|
||||
('process-per-site', True),
|
||||
('single-process', True),
|
||||
])
|
||||
def test_process_model(self, config_stub, monkeypatch, parser,
|
||||
process_model, added):
|
||||
monkeypatch.setattr(configinit.objects, 'backend',
|
||||
usertypes.Backend.QtWebEngine)
|
||||
|
||||
config_stub.val.qt.process_model = process_model
|
||||
parsed = parser.parse_args([])
|
||||
args = configinit.qt_args(parsed)
|
||||
|
||||
if added:
|
||||
assert '--' + process_model in args
|
||||
else:
|
||||
assert '--process-per-site' not in args
|
||||
assert '--single-process' not in args
|
||||
assert '--process-per-site-instance' not in args
|
||||
assert '--process-per-tab' not in args
|
||||
|
||||
@pytest.mark.parametrize('low_end_device_mode, arg', [
|
||||
('auto', None),
|
||||
('always', '--enable-low-end-device-mode'),
|
||||
('never', '--disable-low-end-device-mode'),
|
||||
])
|
||||
def test_low_end_device_mode(self, config_stub, monkeypatch, parser,
|
||||
low_end_device_mode, arg):
|
||||
monkeypatch.setattr(configinit.objects, 'backend',
|
||||
usertypes.Backend.QtWebEngine)
|
||||
|
||||
config_stub.val.qt.low_end_device_mode = low_end_device_mode
|
||||
parsed = parser.parse_args([])
|
||||
args = configinit.qt_args(parsed)
|
||||
|
||||
if arg is None:
|
||||
assert '--enable-low-end-device-mode' not in args
|
||||
assert '--disable-low-end-device-mode' not in args
|
||||
else:
|
||||
assert arg in args
|
||||
|
||||
@pytest.mark.parametrize('referer, arg', [
|
||||
('always', None),
|
||||
('never', '--no-referrers'),
|
||||
('same-domain', '--reduced-referrer-granularity'),
|
||||
])
|
||||
def test_referer(self, config_stub, monkeypatch, parser, referer, arg):
|
||||
monkeypatch.setattr(configinit.objects, 'backend',
|
||||
usertypes.Backend.QtWebEngine)
|
||||
|
||||
config_stub.val.content.headers.referer = referer
|
||||
parsed = parser.parse_args([])
|
||||
args = configinit.qt_args(parsed)
|
||||
|
||||
if arg is None:
|
||||
assert '--no-referrers' not in args
|
||||
assert '--reduced-referrer-granularity' not in args
|
||||
else:
|
||||
assert arg in args
|
||||
|
||||
|
||||
@pytest.mark.parametrize('arg, confval, used', [
|
||||
# overridden by commandline arg
|
||||
|
@ -130,6 +130,26 @@ def test_load_emits_signal(qtbot):
|
||||
gm_manager.load_scripts()
|
||||
|
||||
|
||||
def test_utf8_bom():
|
||||
"""Make sure UTF-8 BOMs are stripped from scripts.
|
||||
|
||||
If we don't strip them, we'll have a BOM in the middle of the file, causing
|
||||
QtWebEngine to not catch the "// ==UserScript==" line.
|
||||
"""
|
||||
script = textwrap.dedent("""
|
||||
\N{BYTE ORDER MARK}// ==UserScript==
|
||||
// @name qutebrowser test userscript
|
||||
// ==/UserScript==
|
||||
""".lstrip('\n'))
|
||||
_save_script(script, 'bom.user.js')
|
||||
gm_manager = greasemonkey.GreasemonkeyManager()
|
||||
|
||||
scripts = gm_manager.all_scripts()
|
||||
assert len(scripts) == 1
|
||||
script = scripts[0]
|
||||
assert '// ==UserScript==' in script.code().splitlines()
|
||||
|
||||
|
||||
def test_required_scripts_are_included(download_stub, tmpdir):
|
||||
test_require_script = textwrap.dedent("""
|
||||
// ==UserScript==
|
||||
|
@ -49,6 +49,19 @@ class TestSqlError:
|
||||
with pytest.raises(exception):
|
||||
sql.raise_sqlite_error("Message", sql_err)
|
||||
|
||||
def test_qtbug_70506(self):
|
||||
"""Test Qt's wrong handling of errors while opening the database.
|
||||
|
||||
Due to https://bugreports.qt.io/browse/QTBUG-70506 we get an error with
|
||||
"out of memory" as string and -1 as error code.
|
||||
"""
|
||||
sql_err = QSqlError("Error opening database",
|
||||
"out of memory",
|
||||
QSqlError.UnknownError,
|
||||
sql.SqliteErrorCode.UNKNOWN)
|
||||
with pytest.raises(sql.SqlEnvironmentError):
|
||||
sql.raise_sqlite_error("Message", sql_err)
|
||||
|
||||
def test_logging(self, caplog):
|
||||
sql_err = QSqlError("driver text", "db text", QSqlError.UnknownError,
|
||||
'23')
|
||||
|
@ -816,3 +816,16 @@ def test_chunk(elems, n, expected):
|
||||
def test_chunk_invalid(n):
|
||||
with pytest.raises(ValueError):
|
||||
list(utils.chunk([], n))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('filename, expected', [
|
||||
('test.jpg', 'image/jpeg'),
|
||||
('test.blabla', 'application/octet-stream'),
|
||||
])
|
||||
def test_guess_mimetype(filename, expected):
|
||||
assert utils.guess_mimetype(filename, fallback=True) == expected
|
||||
|
||||
|
||||
def test_guess_mimetype_no_fallback():
|
||||
with pytest.raises(ValueError):
|
||||
utils.guess_mimetype('test.blabla')
|
||||
|
Loading…
Reference in New Issue
Block a user