Merge branch 'master' into master

This commit is contained in:
Jesko Dujmovic 2018-09-13 22:06:04 +02:00 committed by GitHub
commit 25e396faea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 497 additions and 253 deletions

View File

@ -27,6 +27,11 @@ 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.
Changed
~~~~~~~
@ -48,6 +53,13 @@ Changed
- 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.
Fixed
~~~~~
- Error when passing a substring with spaces to `:tab-take`.
- Greasemonkey scripts which start with an UTF-8 BOM are now handled correctly.
Removed
~~~~~~~
@ -1625,7 +1637,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

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

@ -1488,7 +1488,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>>
@ -1941,8 +1943,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`.

View File

@ -485,7 +485,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 +1184,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

@ -247,11 +247,10 @@ 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:
script = GreasemonkeyScript.parse(
script_file.read(),
filename=script_filename,
)
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
self.add_script(script, force)
self.scripts_reloaded.emit()

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

@ -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
@ -44,10 +43,10 @@ import pkg_resources
from PyQt5.QtCore import QUrlQuery, QUrl
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 +112,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 +125,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):
@ -382,8 +367,7 @@ 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:
@ -531,3 +515,43 @@ 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

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

@ -220,10 +220,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
@ -639,7 +641,6 @@ content.notifications:
content.pdfjs:
default: false
type: Bool
backend: QtWebKit
desc: >-
Allow pdf.js to view PDF files in the browser.

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

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

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,11 +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 qutescheme
from qutebrowser.browser import qutescheme, pdfjs, downloads
class TestJavascriptHandler:
@ -169,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

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