Merge branch 'master' into master

This commit is contained in:
Zarthur 2018-09-20 11:02:27 -06:00 committed by GitHub
commit b41d7ba203
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 1601 additions and 861 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
doc/changelog.asciidoc merge=union

View File

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

@ -16,7 +16,6 @@ __pycache__
/doc/*.html
/README.html
/qutebrowser/html/doc/
/qutebrowser/html/*.html
/.venv*
/.coverage
/htmlcov

View File

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

View File

@ -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 `&lt;3` footnote:[Of course, that says `<3` in HTML.] contributors!
This document contains guidelines for contributing to qutebrowser, as well as

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 = []

View 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]

View File

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

View File

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

View File

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

View File

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

View 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 %}

View 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 %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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']

View File

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

View File

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

View File

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

View File

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

View File

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