Merge branch 'download-page' of https://github.com/Kingdread/qutebrowser into Kingdread-download-page
This commit is contained in:
commit
16e1a65448
@ -102,6 +102,9 @@ The following software and libraries are required to run qutebrowser:
|
||||
* http://pygments.org/[pygments]
|
||||
* http://pyyaml.org/wiki/PyYAML[PyYAML]
|
||||
|
||||
The following libraries are optional and provide a better user experience:
|
||||
* http://cthedot.de/cssutils/[cssutils]
|
||||
|
||||
To generate the documentation for the `:help` command, when using the git
|
||||
repository (rather than a release), http://asciidoc.org/[asciidoc] is needed.
|
||||
|
||||
|
@ -146,13 +146,19 @@ Close the current window.
|
||||
|
||||
[[download]]
|
||||
=== download
|
||||
Syntax: +:download ['url'] ['dest']+
|
||||
Syntax: +:download [*--mhtml*] [*--dest* 'DEST'] ['url'] ['dest-old']+
|
||||
|
||||
Download a given URL, or current page if no URL given.
|
||||
|
||||
The form `:download [url] [dest]` is deprecated, use `:download --dest [dest] [url]` instead.
|
||||
|
||||
==== positional arguments
|
||||
* +'url'+: The URL to download. If not given, download the current page.
|
||||
* +'dest'+: The file path to write the download to, or not given to ask.
|
||||
* +'dest-old'+: (deprecated) Same as dest.
|
||||
|
||||
==== optional arguments
|
||||
* +*-m*+, +*--mhtml*+: Download the current page and all assets as mhtml file.
|
||||
* +*-d*+, +*--dest*+: The file path to write the download to, or not given to ask.
|
||||
|
||||
[[download-cancel]]
|
||||
=== download-cancel
|
||||
|
@ -37,7 +37,7 @@ import pygments.formatters
|
||||
|
||||
from qutebrowser.commands import userscripts, cmdexc, cmdutils, runners
|
||||
from qutebrowser.config import config, configexc
|
||||
from qutebrowser.browser import webelem, inspector, urlmarks
|
||||
from qutebrowser.browser import webelem, inspector, urlmarks, downloads, mhtml
|
||||
from qutebrowser.keyinput import modeman
|
||||
from qutebrowser.utils import (message, usertypes, log, qtutils, urlutils,
|
||||
objreg, utils)
|
||||
@ -1139,22 +1139,66 @@ class CommandDispatcher:
|
||||
cur.inspector.show()
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window')
|
||||
def download(self, url=None, dest=None):
|
||||
def download(self, url=None, dest_old: {'hide': True}=None, *,
|
||||
mhtml_=False, dest=None):
|
||||
"""Download a given URL, or current page if no URL given.
|
||||
|
||||
The form `:download [url] [dest]` is deprecated, use `:download --dest
|
||||
[dest] [url]` instead.
|
||||
|
||||
Args:
|
||||
url: The URL to download. If not given, download the current page.
|
||||
dest_old: (deprecated) Same as dest.
|
||||
dest: The file path to write the download to, or None to ask.
|
||||
mhtml_: Download the current page and all assets as mhtml file.
|
||||
"""
|
||||
if dest_old is not None:
|
||||
message.warning(
|
||||
self._win_id, ":download [url] [dest] is deprecated - use"
|
||||
" download --dest [dest] [url]")
|
||||
if dest is not None:
|
||||
raise cmdexc.CommandError("Can't give two destinations for the"
|
||||
" download.")
|
||||
dest = dest_old
|
||||
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
if url:
|
||||
if mhtml_:
|
||||
raise cmdexc.CommandError("Can only download the current page"
|
||||
" as mhtml.")
|
||||
url = urlutils.qurl_from_user_input(url)
|
||||
urlutils.raise_cmdexc_if_invalid(url)
|
||||
download_manager.get(url, filename=dest)
|
||||
else:
|
||||
page = self._current_widget().page()
|
||||
download_manager.get(self._current_url(), page=page)
|
||||
if mhtml_:
|
||||
self._download_mhtml(dest)
|
||||
else:
|
||||
page = self._current_widget().page()
|
||||
download_manager.get(self._current_url(), page=page,
|
||||
filename=dest)
|
||||
|
||||
def _download_mhtml(self, dest=None):
|
||||
"""Download the current page as a MHTML file, including all assets.
|
||||
|
||||
Args:
|
||||
dest: The file path to write the download to.
|
||||
"""
|
||||
web_view = self._current_widget()
|
||||
if dest is None:
|
||||
suggested_fn = self._current_title() + ".mht"
|
||||
suggested_fn = utils.sanitize_filename(suggested_fn)
|
||||
filename, q = downloads.ask_for_filename(
|
||||
suggested_fn, self._win_id, parent=web_view,
|
||||
)
|
||||
if filename is not None:
|
||||
mhtml.start_download_checked(filename, web_view=web_view)
|
||||
else:
|
||||
q.answered.connect(functools.partial(
|
||||
mhtml.start_download_checked, web_view=web_view))
|
||||
q.ask()
|
||||
else:
|
||||
mhtml.start_download_checked(dest, web_view=web_view)
|
||||
|
||||
@cmdutils.register(instance='command-dispatcher', scope='window',
|
||||
deprecated="Use :download instead.")
|
||||
|
@ -48,8 +48,13 @@ ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
|
||||
|
||||
RetryInfo = collections.namedtuple('RetryInfo', ['request', 'manager'])
|
||||
|
||||
|
||||
DownloadPath = collections.namedtuple('DownloadPath', ['filename',
|
||||
'question'])
|
||||
|
||||
|
||||
# Remember the last used directory
|
||||
_last_used_directory = None
|
||||
last_used_directory = None
|
||||
|
||||
|
||||
# All REFRESH_INTERVAL milliseconds, speeds will be recalculated and downloads
|
||||
@ -57,20 +62,20 @@ _last_used_directory = None
|
||||
REFRESH_INTERVAL = 500
|
||||
|
||||
|
||||
def _download_dir():
|
||||
def download_dir():
|
||||
"""Get the download directory to use."""
|
||||
directory = config.get('storage', 'download-directory')
|
||||
remember_dir = config.get('storage', 'remember-download-directory')
|
||||
|
||||
if remember_dir and _last_used_directory is not None:
|
||||
return _last_used_directory
|
||||
if remember_dir and last_used_directory is not None:
|
||||
return last_used_directory
|
||||
elif directory is None:
|
||||
return standarddir.download()
|
||||
else:
|
||||
return directory
|
||||
|
||||
|
||||
def _path_suggestion(filename):
|
||||
def path_suggestion(filename):
|
||||
"""Get the suggested file path.
|
||||
|
||||
Args:
|
||||
@ -79,15 +84,79 @@ def _path_suggestion(filename):
|
||||
suggestion = config.get('completion', 'download-path-suggestion')
|
||||
if suggestion == 'path':
|
||||
# add trailing '/' if not present
|
||||
return os.path.join(_download_dir(), '')
|
||||
return os.path.join(download_dir(), '')
|
||||
elif suggestion == 'filename':
|
||||
return filename
|
||||
elif suggestion == 'both':
|
||||
return os.path.join(_download_dir(), filename)
|
||||
return os.path.join(download_dir(), filename)
|
||||
else:
|
||||
raise ValueError("Invalid suggestion value {}!".format(suggestion))
|
||||
|
||||
|
||||
def create_full_filename(basename, filename):
|
||||
"""Create a full filename based on the given basename and filename.
|
||||
|
||||
Args:
|
||||
basename: The basename to use if filename is a directory.
|
||||
filename: The path to a folder or file where you want to save.
|
||||
|
||||
Return:
|
||||
The full absolute path, or None if filename creation was not possible.
|
||||
"""
|
||||
if os.path.isabs(filename) and os.path.isdir(filename):
|
||||
# We got an absolute directory from the user, so we save it under
|
||||
# the default filename in that directory.
|
||||
return os.path.join(filename, basename)
|
||||
elif os.path.isabs(filename):
|
||||
# We got an absolute filename from the user, so we save it under
|
||||
# that filename.
|
||||
return filename
|
||||
return None
|
||||
|
||||
|
||||
def ask_for_filename(suggested_filename, win_id, *, parent=None,
|
||||
prompt_download_directory=None):
|
||||
"""Prepare a question for a download-path.
|
||||
|
||||
If a filename can be determined directly, it is returned instead.
|
||||
|
||||
Returns a (filename, question)-namedtuple, in which one component is
|
||||
None. filename is a string, question is a usertypes.Question. The
|
||||
question has a special .ask() method that takes no arguments for
|
||||
convenience, as this function does not yet ask the question, it
|
||||
only prepares it.
|
||||
|
||||
Args:
|
||||
suggested_filename: The "default"-name that is pre-entered as path.
|
||||
win_id: The window where the question will be asked.
|
||||
parent: The parent of the question (a QObject).
|
||||
prompt_download_directory: If this is something else than None, it
|
||||
will overwrite the
|
||||
storage->prompt-download-directory setting.
|
||||
"""
|
||||
if prompt_download_directory is None:
|
||||
prompt_download_directory = config.get('storage',
|
||||
'prompt-download-directory')
|
||||
|
||||
if not prompt_download_directory:
|
||||
return DownloadPath(filename=download_dir(), question=None)
|
||||
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_filename = utils.force_encoding(suggested_filename,
|
||||
encoding)
|
||||
|
||||
q = usertypes.Question(parent)
|
||||
q.text = "Save file to:"
|
||||
q.mode = usertypes.PromptMode.text
|
||||
q.completed.connect(q.deleteLater)
|
||||
q.default = path_suggestion(suggested_filename)
|
||||
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=win_id)
|
||||
q.ask = lambda: message_bridge.ask(q, blocking=False)
|
||||
return DownloadPath(filename=None, question=q)
|
||||
|
||||
|
||||
class DownloadItemStats(QObject):
|
||||
|
||||
"""Statistics (bytes done, total bytes, time, etc.) about a download.
|
||||
@ -201,6 +270,7 @@ class DownloadItem(QObject):
|
||||
fileobj: The file object to download the file to.
|
||||
reply: The QNetworkReply associated with this download.
|
||||
retry_info: A RetryInfo instance.
|
||||
raw_headers: The headers sent by the server.
|
||||
_filename: The filename of the download.
|
||||
_redirects: How many time we were redirected already.
|
||||
_buffer: A BytesIO object to buffer incoming data until we know the
|
||||
@ -255,6 +325,7 @@ class DownloadItem(QObject):
|
||||
self._filename = None
|
||||
self.init_reply(reply)
|
||||
self._win_id = win_id
|
||||
self.raw_headers = {}
|
||||
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, basename=self.basename)
|
||||
@ -354,6 +425,7 @@ class DownloadItem(QObject):
|
||||
reply.finished.connect(self.on_reply_finished)
|
||||
reply.error.connect(self.on_reply_error)
|
||||
reply.readyRead.connect(self.on_ready_read)
|
||||
reply.metaDataChanged.connect(self.on_meta_data_changed)
|
||||
self.retry_info = RetryInfo(request=reply.request(),
|
||||
manager=reply.manager())
|
||||
if not self.fileobj:
|
||||
@ -444,7 +516,7 @@ class DownloadItem(QObject):
|
||||
filename: The full filename to save the download to.
|
||||
None: special value to stop the download.
|
||||
"""
|
||||
global _last_used_directory
|
||||
global last_used_directory
|
||||
if self.fileobj is not None:
|
||||
raise ValueError("fileobj was already set! filename: {}, "
|
||||
"existing: {}, fileobj {}".format(
|
||||
@ -454,13 +526,16 @@ class DownloadItem(QObject):
|
||||
# See https://github.com/The-Compiler/qutebrowser/issues/427
|
||||
encoding = sys.getfilesystemencoding()
|
||||
filename = utils.force_encoding(filename, encoding)
|
||||
if not self._create_full_filename(filename):
|
||||
self._filename = create_full_filename(self.basename, filename)
|
||||
if self._filename is None:
|
||||
# We only got a filename (without directory) or a relative path
|
||||
# from the user, so we append that to the default directory and
|
||||
# try again.
|
||||
self._create_full_filename(os.path.join(_download_dir(), filename))
|
||||
self._filename = create_full_filename(
|
||||
self.basename, os.path.join(download_dir(), filename))
|
||||
|
||||
_last_used_directory = os.path.dirname(self._filename)
|
||||
self.basename = os.path.basename(self._filename)
|
||||
last_used_directory = os.path.dirname(self._filename)
|
||||
|
||||
log.downloads.debug("Setting filename to {}".format(filename))
|
||||
if os.path.isfile(self._filename):
|
||||
@ -477,25 +552,6 @@ class DownloadItem(QObject):
|
||||
else:
|
||||
self._create_fileobj()
|
||||
|
||||
def _create_full_filename(self, filename):
|
||||
"""Try to create the full filename.
|
||||
|
||||
Return:
|
||||
True if the full filename was created, False otherwise.
|
||||
"""
|
||||
if os.path.isabs(filename) and os.path.isdir(filename):
|
||||
# We got an absolute directory from the user, so we save it under
|
||||
# the default filename in that directory.
|
||||
self._filename = os.path.join(filename, self.basename)
|
||||
return True
|
||||
elif os.path.isabs(filename):
|
||||
# We got an absolute filename from the user, so we save it under
|
||||
# that filename.
|
||||
self._filename = filename
|
||||
self.basename = os.path.basename(self._filename)
|
||||
return True
|
||||
return False
|
||||
|
||||
def set_fileobj(self, fileobj):
|
||||
""""Set the file object to write the download to.
|
||||
|
||||
@ -593,6 +649,15 @@ class DownloadItem(QObject):
|
||||
if data is not None:
|
||||
self._buffer.write(data)
|
||||
|
||||
@pyqtSlot()
|
||||
def on_meta_data_changed(self):
|
||||
"""Update the download's metadata."""
|
||||
if self.reply is None:
|
||||
return
|
||||
self.raw_headers = {}
|
||||
for key, value in self.reply.rawHeaderPairs():
|
||||
self.raw_headers[bytes(key)] = bytes(value)
|
||||
|
||||
def _handle_redirect(self):
|
||||
"""Handle a HTTP redirect.
|
||||
|
||||
@ -651,15 +716,10 @@ class DownloadManager(QAbstractListModel):
|
||||
def __repr__(self):
|
||||
return utils.get_repr(self, downloads=len(self.downloads))
|
||||
|
||||
def _prepare_question(self):
|
||||
"""Prepare a Question object to be asked."""
|
||||
q = usertypes.Question(self)
|
||||
q.text = "Save file to:"
|
||||
q.mode = usertypes.PromptMode.text
|
||||
q.completed.connect(q.deleteLater)
|
||||
def _postprocess_question(self, q):
|
||||
"""Postprocess a Question object that is asked."""
|
||||
q.destroyed.connect(functools.partial(self.questions.remove, q))
|
||||
self.questions.append(q)
|
||||
return q
|
||||
|
||||
@pyqtSlot()
|
||||
def update_gui(self):
|
||||
@ -716,11 +776,12 @@ class DownloadManager(QAbstractListModel):
|
||||
QNetworkRequest.AlwaysNetwork)
|
||||
suggested_fn = urlutils.filename_from_url(request.url())
|
||||
|
||||
if prompt_download_directory is None:
|
||||
prompt_download_directory = config.get(
|
||||
'storage', 'prompt-download-directory')
|
||||
if not prompt_download_directory and not fileobj:
|
||||
filename = _download_dir()
|
||||
# We won't need a question if a filename or fileobj is already given
|
||||
if fileobj is None and filename is None:
|
||||
filename, q = ask_for_filename(
|
||||
suggested_fn, self._win_id, parent=self,
|
||||
prompt_download_directory=prompt_download_directory
|
||||
)
|
||||
|
||||
if fileobj is not None or filename is not None:
|
||||
return self.fetch_request(request,
|
||||
@ -728,22 +789,13 @@ class DownloadManager(QAbstractListModel):
|
||||
filename=filename,
|
||||
suggested_filename=suggested_fn,
|
||||
**kwargs)
|
||||
if suggested_fn is None:
|
||||
suggested_fn = 'qutebrowser-download'
|
||||
else:
|
||||
encoding = sys.getfilesystemencoding()
|
||||
suggested_fn = utils.force_encoding(suggested_fn, encoding)
|
||||
|
||||
q = self._prepare_question()
|
||||
q.default = _path_suggestion(suggested_fn)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
q.answered.connect(
|
||||
lambda fn: self.fetch_request(request,
|
||||
filename=fn,
|
||||
suggested_filename=suggested_fn,
|
||||
**kwargs))
|
||||
message_bridge.ask(q, blocking=False)
|
||||
self._postprocess_question(q)
|
||||
q.ask()
|
||||
return None
|
||||
|
||||
def fetch_request(self, request, *, page=None, **kwargs):
|
||||
@ -817,26 +869,33 @@ class DownloadManager(QAbstractListModel):
|
||||
if not self._update_timer.isActive():
|
||||
self._update_timer.start()
|
||||
|
||||
prompt_download_directory = config.get('storage',
|
||||
'prompt-download-directory')
|
||||
if not prompt_download_directory and not fileobj:
|
||||
filename = _download_dir()
|
||||
if fileobj is not None:
|
||||
download.set_fileobj(fileobj)
|
||||
download.autoclose = False
|
||||
return download
|
||||
|
||||
if filename is not None:
|
||||
download.set_filename(filename)
|
||||
elif fileobj is not None:
|
||||
download.set_fileobj(fileobj)
|
||||
download.autoclose = False
|
||||
else:
|
||||
q = self._prepare_question()
|
||||
q.default = _path_suggestion(suggested_filename)
|
||||
q.answered.connect(download.set_filename)
|
||||
q.cancelled.connect(download.cancel)
|
||||
download.cancelled.connect(q.abort)
|
||||
download.error.connect(q.abort)
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=self._win_id)
|
||||
message_bridge.ask(q, blocking=False)
|
||||
return download
|
||||
|
||||
# Neither filename nor fileobj were given, prepare a question
|
||||
filename, q = ask_for_filename(
|
||||
suggested_filename, self._win_id, parent=self,
|
||||
prompt_download_directory=prompt_download_directory,
|
||||
)
|
||||
|
||||
# User doesn't want to be asked, so just use the download_dir
|
||||
if filename is not None:
|
||||
download.set_filename(filename)
|
||||
return download
|
||||
|
||||
# Ask the user for a filename
|
||||
self._postprocess_question(q)
|
||||
q.answered.connect(download.set_filename)
|
||||
q.cancelled.connect(download.cancel)
|
||||
download.cancelled.connect(q.abort)
|
||||
download.error.connect(q.abort)
|
||||
q.ask()
|
||||
|
||||
return download
|
||||
|
||||
|
532
qutebrowser/browser/mhtml.py
Normal file
532
qutebrowser/browser/mhtml.py
Normal file
@ -0,0 +1,532 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Daniel Schadt
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""Utils for writing a MHTML file."""
|
||||
|
||||
import functools
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import collections
|
||||
import uuid
|
||||
import email.policy
|
||||
import email.generator
|
||||
import email.encoders
|
||||
import email.mime.multipart
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
|
||||
from qutebrowser.browser import webelem, downloads
|
||||
from qutebrowser.utils import log, objreg, message, usertypes, utils, urlutils
|
||||
|
||||
try:
|
||||
import cssutils
|
||||
except (ImportError, re.error):
|
||||
# Catching re.error because cssutils in earlier releases (<= 1.0) is broken
|
||||
# on Python 3.5
|
||||
# See https://bitbucket.org/cthedot/cssutils/issues/52
|
||||
cssutils = None
|
||||
|
||||
_File = collections.namedtuple('_File',
|
||||
['content', 'content_type', 'content_location',
|
||||
'transfer_encoding'])
|
||||
|
||||
|
||||
_CSS_URL_PATTERNS = [re.compile(x) for x in [
|
||||
r"@import\s+'(?P<url>[^']+)'",
|
||||
r'@import\s+"(?P<url>[^"]+)"',
|
||||
r'''url\((?P<url>[^'"][^)]*)\)''',
|
||||
r'url\("(?P<url>[^"]+)"\)',
|
||||
r"url\('(?P<url>[^']+)'\)",
|
||||
]]
|
||||
|
||||
|
||||
def _get_css_imports_regex(data):
|
||||
"""Return all assets that are referenced in the given CSS document.
|
||||
|
||||
The returned URLs are relative to the stylesheet's URL.
|
||||
|
||||
Args:
|
||||
data: The content of the stylesheet to scan as string.
|
||||
"""
|
||||
urls = []
|
||||
for pattern in _CSS_URL_PATTERNS:
|
||||
for match in pattern.finditer(data):
|
||||
url = match.group("url")
|
||||
if url:
|
||||
urls.append(url)
|
||||
return urls
|
||||
|
||||
|
||||
def _get_css_imports_cssutils(data, inline=False):
|
||||
"""Return all assets that are referenced in the given CSS document.
|
||||
|
||||
The returned URLs are relative to the stylesheet's URL.
|
||||
|
||||
Args:
|
||||
data: The content of the stylesheet to scan as string.
|
||||
inline: True if the argument is a inline HTML style attribute.
|
||||
"""
|
||||
# We don't care about invalid CSS data, this will only litter the log
|
||||
# output with CSS errors
|
||||
parser = cssutils.CSSParser(loglevel=100,
|
||||
fetcher=lambda url: (None, ""), validate=False)
|
||||
if not inline:
|
||||
sheet = parser.parseString(data)
|
||||
return list(cssutils.getUrls(sheet))
|
||||
else:
|
||||
urls = []
|
||||
declaration = parser.parseStyle(data)
|
||||
# prop = background, color, margin, ...
|
||||
for prop in declaration:
|
||||
# value = red, 10px, url(foobar), ...
|
||||
for value in prop.propertyValue:
|
||||
if isinstance(value, cssutils.css.URIValue):
|
||||
if value.uri:
|
||||
urls.append(value.uri)
|
||||
return urls
|
||||
|
||||
|
||||
def _get_css_imports(data, inline=False):
|
||||
"""Return all assets that are referenced in the given CSS document.
|
||||
|
||||
The returned URLs are relative to the stylesheet's URL.
|
||||
|
||||
Args:
|
||||
data: The content of the stylesheet to scan as string.
|
||||
inline: True if the argument is a inline HTML style attribute.
|
||||
"""
|
||||
if cssutils is None:
|
||||
return _get_css_imports_regex(data)
|
||||
else:
|
||||
return _get_css_imports_cssutils(data, inline)
|
||||
|
||||
|
||||
def _check_rel(element):
|
||||
"""Return true if the element's rel attribute fits our criteria.
|
||||
|
||||
rel has to contain 'stylesheet' or 'icon'. Also returns True if the rel
|
||||
attribute is unset.
|
||||
|
||||
Args:
|
||||
element: The WebElementWrapper which should be checked.
|
||||
"""
|
||||
if 'rel' not in element:
|
||||
return True
|
||||
must_have = {'stylesheet', 'icon'}
|
||||
rels = [rel.lower() for rel in element['rel'].split(' ')]
|
||||
return any(rel in rels for rel in must_have)
|
||||
|
||||
|
||||
MHTMLPolicy = email.policy.default.clone(linesep='\r\n', max_line_length=0)
|
||||
|
||||
|
||||
# Encode the file using base64 encoding.
|
||||
E_BASE64 = email.encoders.encode_base64
|
||||
|
||||
|
||||
# Encode the file using MIME quoted-printable encoding.
|
||||
E_QUOPRI = email.encoders.encode_quopri
|
||||
|
||||
|
||||
class MHTMLWriter:
|
||||
|
||||
"""A class for outputting multiple files to a MHTML document.
|
||||
|
||||
Attributes:
|
||||
root_content: The root content as bytes.
|
||||
content_location: The url of the page as str.
|
||||
content_type: The MIME-type of the root content as str.
|
||||
_files: Mapping of location->_File namedtuple.
|
||||
"""
|
||||
|
||||
def __init__(self, root_content, content_location, content_type):
|
||||
self.root_content = root_content
|
||||
self.content_location = content_location
|
||||
self.content_type = content_type
|
||||
self._files = {}
|
||||
|
||||
def add_file(self, location, content, content_type=None,
|
||||
transfer_encoding=E_QUOPRI):
|
||||
"""Add a file to the given MHTML collection.
|
||||
|
||||
Args:
|
||||
location: The original location (URL) of the file.
|
||||
content: The binary content of the file.
|
||||
content_type: The MIME-type of the content (if available)
|
||||
transfer_encoding: The transfer encoding to use for this file.
|
||||
"""
|
||||
self._files[location] = _File(
|
||||
content=content, content_type=content_type,
|
||||
content_location=location, transfer_encoding=transfer_encoding,
|
||||
)
|
||||
|
||||
def write_to(self, fp):
|
||||
"""Output the MHTML file to the given file-like object.
|
||||
|
||||
Args:
|
||||
fp: The file-object, opened in "wb" mode.
|
||||
"""
|
||||
msg = email.mime.multipart.MIMEMultipart(
|
||||
'related', '---=_qute-{}'.format(uuid.uuid4()))
|
||||
|
||||
root = self._create_root_file()
|
||||
msg.attach(root)
|
||||
|
||||
for _, file_data in sorted(self._files.items()):
|
||||
msg.attach(self._create_file(file_data))
|
||||
|
||||
gen = email.generator.BytesGenerator(fp, policy=MHTMLPolicy)
|
||||
gen.flatten(msg)
|
||||
|
||||
def _create_root_file(self):
|
||||
"""Return the root document as MIMEMultipart."""
|
||||
root_file = _File(
|
||||
content=self.root_content, content_type=self.content_type,
|
||||
content_location=self.content_location, transfer_encoding=E_QUOPRI,
|
||||
)
|
||||
return self._create_file(root_file)
|
||||
|
||||
def _create_file(self, f):
|
||||
"""Return the single given file as MIMEMultipart."""
|
||||
msg = email.mime.multipart.MIMEMultipart()
|
||||
msg['Content-Location'] = f.content_location
|
||||
# Get rid of the default type multipart/mixed
|
||||
del msg['Content-Type']
|
||||
if f.content_type:
|
||||
msg.set_type(f.content_type)
|
||||
msg.set_payload(f.content)
|
||||
f.transfer_encoding(msg)
|
||||
return msg
|
||||
|
||||
|
||||
class _Downloader:
|
||||
|
||||
"""A class to download whole websites.
|
||||
|
||||
Attributes:
|
||||
web_view: The QWebView which contains the website that will be saved.
|
||||
dest: Destination filename.
|
||||
writer: The MHTMLWriter object which is used to save the page.
|
||||
loaded_urls: A set of QUrls of finished asset downloads.
|
||||
pending_downloads: A set of unfinished (url, DownloadItem) tuples.
|
||||
_finished_file: A flag indicating if the file has already been
|
||||
written.
|
||||
_used: A flag indicating if the downloader has already been used.
|
||||
_win_id: The window this downloader belongs to.
|
||||
"""
|
||||
|
||||
def __init__(self, web_view, dest):
|
||||
self.web_view = web_view
|
||||
self.dest = dest
|
||||
self.writer = None
|
||||
self.loaded_urls = {web_view.url()}
|
||||
self.pending_downloads = set()
|
||||
self._finished_file = False
|
||||
self._used = False
|
||||
self._win_id = web_view.win_id
|
||||
|
||||
def run(self):
|
||||
"""Download and save the page.
|
||||
|
||||
The object must not be reused, you should create a new one if
|
||||
you want to download another page.
|
||||
"""
|
||||
if self._used:
|
||||
raise ValueError("Downloader already used")
|
||||
self._used = True
|
||||
web_url = self.web_view.url()
|
||||
web_frame = self.web_view.page().mainFrame()
|
||||
|
||||
self.writer = MHTMLWriter(
|
||||
web_frame.toHtml().encode('utf-8'),
|
||||
content_location=urlutils.encoded_url(web_url),
|
||||
# I've found no way of getting the content type of a QWebView, but
|
||||
# since we're using .toHtml, it's probably safe to say that the
|
||||
# content-type is HTML
|
||||
content_type='text/html; charset="UTF-8"',
|
||||
)
|
||||
# Currently only downloading <link> (stylesheets), <script>
|
||||
# (javascript) and <img> (image) elements.
|
||||
elements = web_frame.findAllElements('link, script, img')
|
||||
|
||||
for element in elements:
|
||||
element = webelem.WebElementWrapper(element)
|
||||
# Websites are free to set whatever rel=... attribute they want.
|
||||
# We just care about stylesheets and icons.
|
||||
if not _check_rel(element):
|
||||
continue
|
||||
if 'src' in element:
|
||||
element_url = element['src']
|
||||
elif 'href' in element:
|
||||
element_url = element['href']
|
||||
else:
|
||||
# Might be a local <script> tag or something else
|
||||
continue
|
||||
absolute_url = web_url.resolved(QUrl(element_url))
|
||||
self._fetch_url(absolute_url)
|
||||
|
||||
styles = web_frame.findAllElements('style')
|
||||
for style in styles:
|
||||
style = webelem.WebElementWrapper(style)
|
||||
# The Mozilla Developer Network says:
|
||||
# type: This attribute defines the styling language as a MIME type
|
||||
# (charset should not be specified). This attribute is optional and
|
||||
# default to text/css if it's missing.
|
||||
# https://developer.mozilla.org/en/docs/Web/HTML/Element/style
|
||||
if 'type' in style and style['type'] != 'text/css':
|
||||
continue
|
||||
for element_url in _get_css_imports(str(style)):
|
||||
self._fetch_url(web_url.resolved(QUrl(element_url)))
|
||||
|
||||
# Search for references in inline styles
|
||||
for element in web_frame.findAllElements('[style]'):
|
||||
element = webelem.WebElementWrapper(element)
|
||||
style = element['style']
|
||||
for element_url in _get_css_imports(style, inline=True):
|
||||
self._fetch_url(web_url.resolved(QUrl(element_url)))
|
||||
|
||||
# Shortcut if no assets need to be downloaded, otherwise the file would
|
||||
# never be saved. Also might happen if the downloads are fast enough to
|
||||
# complete before connecting their finished signal.
|
||||
self._collect_zombies()
|
||||
if not self.pending_downloads and not self._finished_file:
|
||||
self._finish_file()
|
||||
|
||||
def _fetch_url(self, url):
|
||||
"""Download the given url and add the file to the collection.
|
||||
|
||||
Args:
|
||||
url: The file to download as QUrl.
|
||||
"""
|
||||
if url.scheme() not in {'http', 'https'}:
|
||||
return
|
||||
# Prevent loading an asset twice
|
||||
if url in self.loaded_urls:
|
||||
return
|
||||
self.loaded_urls.add(url)
|
||||
|
||||
log.downloads.debug("loading asset at {}".format(url))
|
||||
|
||||
# Using the download manager to download host-blocked urls might crash
|
||||
# qute, see the comments/discussion on
|
||||
# https://github.com/The-Compiler/qutebrowser/pull/962#discussion_r40256987
|
||||
# and https://github.com/The-Compiler/qutebrowser/issues/1053
|
||||
host_blocker = objreg.get('host-blocker')
|
||||
if host_blocker.is_blocked(url):
|
||||
log.downloads.debug("Skipping {}, host-blocked".format(url))
|
||||
# We still need an empty file in the output, QWebView can be pretty
|
||||
# picky about displaying a file correctly when not all assets are
|
||||
# at least referenced in the mhtml file.
|
||||
self.writer.add_file(urlutils.encoded_url(url), b'')
|
||||
return
|
||||
|
||||
download_manager = objreg.get('download-manager', scope='window',
|
||||
window=self._win_id)
|
||||
item = download_manager.get(url, fileobj=_NoCloseBytesIO(),
|
||||
auto_remove=True)
|
||||
self.pending_downloads.add((url, item))
|
||||
item.finished.connect(
|
||||
functools.partial(self._finished, url, item))
|
||||
item.error.connect(
|
||||
functools.partial(self._error, url, item))
|
||||
item.cancelled.connect(
|
||||
functools.partial(self._error, url, item))
|
||||
|
||||
def _finished(self, url, item):
|
||||
"""Callback when a single asset is downloaded.
|
||||
|
||||
Args:
|
||||
url: The original url of the asset as QUrl.
|
||||
item: The DownloadItem given by the DownloadManager
|
||||
"""
|
||||
self.pending_downloads.remove((url, item))
|
||||
mime = item.raw_headers.get(b'Content-Type', b'')
|
||||
|
||||
# Note that this decoding always works and doesn't produce errors
|
||||
# RFC 7230 (https://tools.ietf.org/html/rfc7230) states:
|
||||
# Historically, HTTP has allowed field content with text in the
|
||||
# ISO-8859-1 charset [ISO-8859-1], supporting other charsets only
|
||||
# through use of [RFC2047] encoding. In practice, most HTTP header
|
||||
# field values use only a subset of the US-ASCII charset [USASCII].
|
||||
# Newly defined header fields SHOULD limit their field values to
|
||||
# US-ASCII octets. A recipient SHOULD treat other octets in field
|
||||
# content (obs-text) as opaque data.
|
||||
mime = mime.decode('iso-8859-1')
|
||||
|
||||
if mime.lower() == 'text/css' or url.fileName().endswith('.css'):
|
||||
# We can't always assume that CSS files are UTF-8, but CSS files
|
||||
# shouldn't contain many non-ASCII characters anyway (in most
|
||||
# cases). Using "ignore" lets us decode the file even if it's
|
||||
# invalid UTF-8 data.
|
||||
# The file written to the MHTML file won't be modified by this
|
||||
# decoding, since there we're taking the original bytestream.
|
||||
try:
|
||||
css_string = item.fileobj.getvalue().decode('utf-8')
|
||||
except UnicodeDecodeError:
|
||||
log.downloads.warning("Invalid UTF-8 data in {}".format(url))
|
||||
css_string = item.fileobj.getvalue().decode('utf-8', 'ignore')
|
||||
import_urls = _get_css_imports(css_string)
|
||||
for import_url in import_urls:
|
||||
absolute_url = url.resolved(QUrl(import_url))
|
||||
self._fetch_url(absolute_url)
|
||||
|
||||
encode = E_QUOPRI if mime.startswith('text/') else E_BASE64
|
||||
# Our MHTML handler refuses non-ASCII headers. This will replace every
|
||||
# non-ASCII char with '?'. This is probably okay, as official Content-
|
||||
# Type headers contain ASCII only anyway. Anything else is madness.
|
||||
mime = utils.force_encoding(mime, 'ascii')
|
||||
self.writer.add_file(urlutils.encoded_url(url),
|
||||
item.fileobj.getvalue(), mime, encode)
|
||||
item.fileobj.actual_close()
|
||||
if self.pending_downloads:
|
||||
return
|
||||
self._finish_file()
|
||||
|
||||
def _error(self, url, item, *_args):
|
||||
"""Callback when a download error occurred.
|
||||
|
||||
Args:
|
||||
url: The orignal url of the asset as QUrl.
|
||||
item: The DownloadItem given by the DownloadManager.
|
||||
"""
|
||||
try:
|
||||
self.pending_downloads.remove((url, item))
|
||||
except KeyError:
|
||||
# This might happen if .collect_zombies() calls .finished() and the
|
||||
# error handler will be called after .collect_zombies
|
||||
log.downloads.debug("Oops! Download already gone: {}".format(item))
|
||||
return
|
||||
item.fileobj.actual_close()
|
||||
# Add a stub file, see comment in .fetch_url() for more information
|
||||
self.writer.add_file(urlutils.encoded_url(url), b'')
|
||||
if self.pending_downloads:
|
||||
return
|
||||
self._finish_file()
|
||||
|
||||
def _finish_file(self):
|
||||
"""Save the file to the filename given in __init__."""
|
||||
if self._finished_file:
|
||||
log.downloads.debug("finish_file called twice, ignored!")
|
||||
return
|
||||
self._finished_file = True
|
||||
log.downloads.debug("All assets downloaded, ready to finish off!")
|
||||
try:
|
||||
with open(self.dest, 'wb') as file_output:
|
||||
self.writer.write_to(file_output)
|
||||
except OSError as error:
|
||||
message.error(self._win_id,
|
||||
"Could not save file: {}".format(error))
|
||||
return
|
||||
log.downloads.debug("File successfully written.")
|
||||
message.info(self._win_id, "Page saved as {}".format(self.dest))
|
||||
|
||||
def _collect_zombies(self):
|
||||
"""Collect done downloads and add their data to the MHTML file.
|
||||
|
||||
This is needed if a download finishes before attaching its
|
||||
finished signal.
|
||||
"""
|
||||
items = set((url, item) for url, item in self.pending_downloads
|
||||
if item.done)
|
||||
log.downloads.debug("Zombie downloads: {}".format(items))
|
||||
for url, item in items:
|
||||
self._finished(url, item)
|
||||
|
||||
|
||||
class _NoCloseBytesIO(io.BytesIO): # pylint: disable=no-init
|
||||
|
||||
"""BytesIO that can't be .closed().
|
||||
|
||||
This is needed to prevent the DownloadManager from closing the stream, thus
|
||||
discarding the data.
|
||||
"""
|
||||
|
||||
def close(self):
|
||||
"""Do nothing."""
|
||||
pass
|
||||
|
||||
def actual_close(self):
|
||||
"""Close the stream."""
|
||||
super().close()
|
||||
|
||||
|
||||
def _start_download(dest, web_view):
|
||||
"""Start downloading the current page and all assets to a MHTML file.
|
||||
|
||||
This will overwrite dest if it already exists.
|
||||
|
||||
Args:
|
||||
dest: The filename where the resulting file should be saved.
|
||||
win_id, tab_id: Specify the tab whose page should be loaded.
|
||||
"""
|
||||
loader = _Downloader(web_view, dest)
|
||||
loader.run()
|
||||
|
||||
|
||||
def start_download_checked(dest, web_view):
|
||||
"""First check if dest is already a file, then start the download.
|
||||
|
||||
Args:
|
||||
dest: The filename where the resulting file should be saved.
|
||||
web_view: Specify the webview whose page should be loaded.
|
||||
"""
|
||||
# The default name is 'page title.mht'
|
||||
title = web_view.title()
|
||||
default_name = utils.sanitize_filename(title + '.mht')
|
||||
|
||||
# Remove characters which cannot be expressed in the file system encoding
|
||||
encoding = sys.getfilesystemencoding()
|
||||
default_name = utils.force_encoding(default_name, encoding)
|
||||
dest = utils.force_encoding(dest, encoding)
|
||||
|
||||
dest = os.path.expanduser(dest)
|
||||
|
||||
# See if we already have an absolute path
|
||||
path = downloads.create_full_filename(default_name, dest)
|
||||
if path is None:
|
||||
# We still only have a relative path, prepend download_dir and
|
||||
# try again.
|
||||
path = downloads.create_full_filename(
|
||||
default_name, os.path.join(downloads.download_dir(), dest))
|
||||
downloads.last_used_directory = os.path.dirname(path)
|
||||
|
||||
# Avoid downloading files if we can't save the output anyway...
|
||||
# Yes, this is prone to race conditions, but we're checking again before
|
||||
# saving the file anyway.
|
||||
if not os.path.isdir(os.path.dirname(path)):
|
||||
folder = os.path.dirname(path)
|
||||
message.error(web_view.win_id,
|
||||
"Directory {} does not exist.".format(folder))
|
||||
return
|
||||
|
||||
if not os.path.isfile(path):
|
||||
_start_download(path, web_view=web_view)
|
||||
return
|
||||
|
||||
q = usertypes.Question()
|
||||
q.mode = usertypes.PromptMode.yesno
|
||||
q.text = "{} exists. Overwrite?".format(path)
|
||||
q.completed.connect(q.deleteLater)
|
||||
q.answered_yes.connect(functools.partial(
|
||||
_start_download, path, web_view=web_view))
|
||||
message_bridge = objreg.get('message-bridge', scope='window',
|
||||
window=web_view.win_id)
|
||||
message_bridge.ask(q, blocking=False)
|
@ -159,7 +159,7 @@ class WebView(QWebView):
|
||||
return page
|
||||
|
||||
def __repr__(self):
|
||||
url = utils.elide(self.url().toDisplayString(), 100)
|
||||
url = utils.elide(self.url().toDisplayString(QUrl.EncodeUnicode), 100)
|
||||
return utils.get_repr(self, tab_id=self.tab_id, url=url)
|
||||
|
||||
def __del__(self):
|
||||
|
@ -432,6 +432,15 @@ def same_domain(url1, url2):
|
||||
return domain1 == domain2
|
||||
|
||||
|
||||
def encoded_url(url):
|
||||
"""Return the fully encoded url as string.
|
||||
|
||||
Args:
|
||||
url: The url to encode as QUrl.
|
||||
"""
|
||||
return bytes(url.toEncoded()).decode('ascii')
|
||||
|
||||
|
||||
class IncDecError(Exception):
|
||||
|
||||
"""Exception raised by incdec_number on problems.
|
||||
|
@ -705,6 +705,27 @@ def force_encoding(text, encoding):
|
||||
return text.encode(encoding, errors='replace').decode(encoding)
|
||||
|
||||
|
||||
def sanitize_filename(name, replacement='_'):
|
||||
"""Replace invalid filename characters.
|
||||
|
||||
Note: This should be used for the basename, as it also removes the path
|
||||
separator.
|
||||
|
||||
Args:
|
||||
name: The filename.
|
||||
replacement: The replacement character (or None).
|
||||
"""
|
||||
if replacement is None:
|
||||
replacement = ''
|
||||
# Bad characters taken from Windows, there are even fewer on Linux
|
||||
# See also
|
||||
# https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
|
||||
bad_chars = '\\/:*?"<>|'
|
||||
for bad_char in bad_chars:
|
||||
name = name.replace(bad_char, replacement)
|
||||
return name
|
||||
|
||||
|
||||
def newest_slice(iterable, count):
|
||||
"""Get an iterable for the n newest items of the given iterable.
|
||||
|
||||
|
@ -133,6 +133,7 @@ def _module_versions():
|
||||
('jinja2', ['__version__']),
|
||||
('pygments', ['__version__']),
|
||||
('yaml', ['__version__']),
|
||||
('cssutils', ['__version__']),
|
||||
])
|
||||
for name, attributes in modules.items():
|
||||
try:
|
||||
|
@ -5,3 +5,4 @@ pyPEG2==2.15.2
|
||||
PyYAML==3.11
|
||||
colorama==0.3.3
|
||||
colorlog==2.6.0
|
||||
cssutils==1.0.1
|
||||
|
@ -55,7 +55,9 @@ def get_build_exe_options():
|
||||
opts = freeze.get_build_exe_options(skip_html=True)
|
||||
opts['includes'] += pytest.freeze_includes() # pylint: disable=no-member
|
||||
opts['includes'] += ['unittest.mock', 'PyQt5.QtTest', 'hypothesis', 'bs4',
|
||||
'httpbin', 'jinja2.ext', 'xvfbwrapper']
|
||||
'httpbin', 'jinja2.ext', 'xvfbwrapper',
|
||||
'cherrypy.wsgiserver',
|
||||
'cherrypy.wsgiserver.wsgiserver3']
|
||||
|
||||
httpbin_dir = os.path.dirname(httpbin.__file__)
|
||||
opts['include_files'] += [
|
||||
|
@ -80,6 +80,7 @@ def whitelist_generator():
|
||||
# https://bitbucket.org/jendrikseipp/vulture/issues/10/
|
||||
yield 'qutebrowser.misc.utilcmds.pyeval_output'
|
||||
yield 'utils.use_color'
|
||||
yield 'qutebrowser.browser.mhtml.last_used_directory'
|
||||
|
||||
# Other false-positives
|
||||
yield ('qutebrowser.completion.models.sortfilter.CompletionFilterModel().'
|
||||
|
BIN
tests/integration/data/downloads/mhtml/complex/background.png
Normal file
BIN
tests/integration/data/downloads/mhtml/complex/background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
3
tests/integration/data/downloads/mhtml/complex/base.css
Normal file
3
tests/integration/data/downloads/mhtml/complex/base.css
Normal file
@ -0,0 +1,3 @@
|
||||
div.fancy {
|
||||
background-color: url("div-image.png");
|
||||
}
|
38
tests/integration/data/downloads/mhtml/complex/complex.html
Normal file
38
tests/integration/data/downloads/mhtml/complex/complex.html
Normal file
@ -0,0 +1,38 @@
|
||||
<html>
|
||||
<head>
|
||||
<!-- make sure external css is included -->
|
||||
<link rel="stylesheet" href="base.css">
|
||||
<!-- make sure <style> is parsed -->
|
||||
<style>
|
||||
@import "extern-css.css";
|
||||
body {
|
||||
background-image: url("background.png");
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
text-shadow: -1px 0 black, 0 1px black, 1px 0 black, 0 -1px black;
|
||||
}
|
||||
</style>
|
||||
<!-- don't parse non-CSS styles -->
|
||||
<style rel="stylesheet" type="text/qss">
|
||||
@import "actually-it's-css";
|
||||
</style>
|
||||
<!-- make sure icons are included -->
|
||||
<link rel="icon" href="favicon.png">
|
||||
<!-- make sure authors are NOT included -->
|
||||
<link rel="author" href="author.html">
|
||||
<!-- make sure scripts are included -->
|
||||
<script type="text/javascript" src="script.js"></script>
|
||||
<!-- ...but don't crash on scripts without src -->
|
||||
<script>
|
||||
var l = 1+1;
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<!-- include a normal image -->
|
||||
<img src="image.gif">
|
||||
<!-- make sure inline styles are parsed -->
|
||||
<p style="background-image: url('inline.png')">foobar</p>
|
||||
<!-- make sure inline.png is only be requested once -->
|
||||
<img src="inline.png">
|
||||
</body>
|
||||
</html>
|
569
tests/integration/data/downloads/mhtml/complex/complex.mht
Normal file
569
tests/integration/data/downloads/mhtml/complex/complex.mht
Normal file
@ -0,0 +1,569 @@
|
||||
Content-Type: multipart/related; boundary="---=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384"
|
||||
MIME-Version: 1.0
|
||||
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/complex/complex.html
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/html; charset="UTF-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<html><head>
|
||||
=20=20=20=20=20=20=20=20<!--=20make=20sure=20external=20css=20is=20included=
|
||||
=20-->
|
||||
=20=20=20=20=20=20=20=20<link=20rel=3D"stylesheet"=20href=3D"base.css">
|
||||
=20=20=20=20=20=20=20=20<!--=20make=20sure=20<style>=20is=20parsed=20-->
|
||||
=20=20=20=20=20=20=20=20<style>
|
||||
@import=20"extern-css.css";
|
||||
body=20{
|
||||
=20=20=20=20background-image:=20url("background.png");
|
||||
=20=20=20=20font-weight:=20bold;
|
||||
=20=20=20=20color:=20white;
|
||||
=20=20=20=20text-shadow:=20-1px=200=20black,=200=201px=20black,=201px=200=
|
||||
=20black,=200=20-1px=20black;
|
||||
}
|
||||
=20=20=20=20=20=20=20=20</style>
|
||||
=20=20=20=20=20=20=20=20<!--=20don't=20parse=20non-CSS=20styles=20-->
|
||||
=20=20=20=20=20=20=20=20<style=20rel=3D"stylesheet"=20type=3D"text/qss">
|
||||
@import=20"actually-it's-css";
|
||||
=20=20=20=20=20=20=20=20</style>
|
||||
=20=20=20=20=20=20=20=20<!--=20make=20sure=20icons=20are=20included=20-->
|
||||
=20=20=20=20=20=20=20=20<link=20rel=3D"icon"=20href=3D"favicon.png">
|
||||
=20=20=20=20=20=20=20=20<!--=20make=20sure=20authors=20are=20NOT=20included=
|
||||
=20-->
|
||||
=20=20=20=20=20=20=20=20<link=20rel=3D"author"=20href=3D"author.html">
|
||||
=20=20=20=20=20=20=20=20<!--=20make=20sure=20scripts=20are=20included=20-->
|
||||
=20=20=20=20=20=20=20=20<script=20type=3D"text/javascript"=20src=3D"script.=
|
||||
js"></script>
|
||||
=20=20=20=20=20=20=20=20<!--=20...but=20don't=20crash=20on=20scripts=20with=
|
||||
out=20src=20-->
|
||||
=20=20=20=20=20=20=20=20<script>
|
||||
var=20l=20=3D=201+1;
|
||||
=20=20=20=20=20=20=20=20</script>
|
||||
=20=20=20=20</head>
|
||||
=20=20=20=20<body>
|
||||
=20=20=20=20=20=20=20=20<!--=20include=20a=20normal=20image=20-->
|
||||
=20=20=20=20=20=20=20=20<img=20src=3D"image.gif">
|
||||
=20=20=20=20=20=20=20=20<!--=20make=20sure=20inline=20styles=20are=20parsed=
|
||||
=20-->
|
||||
=20=20=20=20=20=20=20=20<p=20style=3D"background-image:=20url('inline.png')=
|
||||
">foobar</p>
|
||||
=20=20=20=20=20=20=20=20<!--=20make=20sure=20inline.png=20is=20only=20be=20=
|
||||
requested=20once=20-->
|
||||
=20=20=20=20=20=20=20=20<img=20src=3D"inline.png">
|
||||
=20=20=20=20
|
||||
|
||||
</body></html>
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/complex/background.png
|
||||
MIME-Version: 1.0
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
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
|
||||
4n8BgjXlKCMEM1EAAAAASUVORK5CYII=
|
||||
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/complex/base.css
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/css; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
div.fancy=20{
|
||||
=20=20=20=20background-color:=20url("div-image.png");
|
||||
}
|
||||
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/complex/div-image.png
|
||||
MIME-Version: 1.0
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
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
|
||||
m+48GgAAAABJRU5ErkJggg==
|
||||
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/complex/extern-css.css
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/css; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
@import=20"external-in-external.css";
|
||||
p=20{
|
||||
=20=20=20=20font-family:=20"Monospace";
|
||||
}
|
||||
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/complex/external-in-external.css
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/css; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
img=20{
|
||||
=20=20=20=20width:=20100%;
|
||||
}
|
||||
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/complex/favicon.png
|
||||
MIME-Version: 1.0
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
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
|
||||
rkJggg==
|
||||
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/complex/image.gif
|
||||
MIME-Version: 1.0
|
||||
Content-Type: image/gif
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
R0lGODlhhQBkAPcAAAAAAAMADAoBDwIBFAsCEwQBGwsEHAwIGhIFHRUGGgYBIgsEJAwEKw4JKBMF
|
||||
IxMKJRIELBoGLBQLKxwKLBoIJRsRLRcQJw0IMxQGMxULMxwMNBQLOxwNOxgGNR0SNB0SOxsVPCML
|
||||
LSIZLCQLMywMMyYOOSMUPCsTPCQZPSobPCMXNTcUOzEMMCshOzEmPEUUOxoOQhQMQx0SRB0USxcT
|
||||
RhwVUiMVRCkWQiUaRCsbRCIWSiUaTCsdSzIaRDUcSCUbUyseUiQbWSwhQy4hTDMkTTcnSi0hUywi
|
||||
WyYhXDMkUzUpUzoqVTMmWzUqXDsrWzkmVT4yXT0yVD0yTy0lYTQrYTstYjEnZT0zZDszazw1dEcc
|
||||
REknSVIsTUIuXEUrVVQqVUIzXEc3Wlg3WU8zTUM0Y0Q6ZUs7ZEQ2a0U6a0s8a0w3Z1k5ZUY7c0k1
|
||||
ZmI9ZGA7X1ZHXE5CZU1CbEdAZ1JDbFhIaUxCc0xEe1JFc1ZKdFpMdFRKeVtMe1dFdV1SfF1SdGRH
|
||||
aWNMdWdKdWJTfWdXeXJYe2VWbmxkenxqf0U+gl1Tg1xTiFNKhGNUgmVZg2pdhGRai2tdi2xUgXNc
|
||||
h2pekmtihW1ijHJkjHRqjHlniGxjk3JlknRqk3lslXVrm3ltmnxylXxzm396m3xzinx1o3x2pnRs
|
||||
oYN1m4R7m4Z5l4Jxj4R8pIl9pYR7qIyBnYyKmo2Hk4yDpIaCrIyErIyHqJOLrJSJqJmTqo2LtIqF
|
||||
sZONtJeMs5WRtJyUtJWTuZyZvJyXt6Kas6Kcu6SataukvKilubSxvJ+cwKOewqaiw6qlwqypxa2p
|
||||
ybKsw7Ouy7CnwbWyy7u6zLe1yL270rq10sG9y8G91L/Ex8XCzcTC1cbD2snG3MzL3cjL09LT3dXb
|
||||
3c7P2tHO4dPU49zd5dvc6tfY5c3N4N7h69zj5d/i8OHl7OTq7uXr6u797+n07eXr8eru8+vz9O/9
|
||||
8O72+fD+8fX6+vD/7wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAAAAAAALAAAAACFAGQA
|
||||
AAj+AAEIHEiwoMGDCBMqXMiwocOHECNKnEixosWLGDNq3Mixo8ePIEOKHEmypMmTKFOqXMmypcuX
|
||||
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
|
||||
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/complex/inline.png
|
||||
MIME-Version: 1.0
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
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
|
||||
cmeb7jwaAAAAAElFTkSuQmCC
|
||||
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/complex/script.js
|
||||
MIME-Version: 1.0
|
||||
Content-Type: application/javascript
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
ZnVuY3Rpb24gbm9vcCgpIHt9Cm5vb3AoKTsK
|
||||
|
||||
-----=_qute-986aec6d-ef0d-4d36-8a81-0b9c0f826384--
|
BIN
tests/integration/data/downloads/mhtml/complex/div-image.png
Normal file
BIN
tests/integration/data/downloads/mhtml/complex/div-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
@ -0,0 +1,4 @@
|
||||
@import "external-in-external.css";
|
||||
p {
|
||||
font-family: "Monospace";
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
BIN
tests/integration/data/downloads/mhtml/complex/favicon.png
Normal file
BIN
tests/integration/data/downloads/mhtml/complex/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
tests/integration/data/downloads/mhtml/complex/image.gif
Normal file
BIN
tests/integration/data/downloads/mhtml/complex/image.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.3 KiB |
BIN
tests/integration/data/downloads/mhtml/complex/inline.png
Normal file
BIN
tests/integration/data/downloads/mhtml/complex/inline.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
9
tests/integration/data/downloads/mhtml/complex/requests
Normal file
9
tests/integration/data/downloads/mhtml/complex/requests
Normal file
@ -0,0 +1,9 @@
|
||||
background.png
|
||||
base.css
|
||||
div-image.png
|
||||
extern-css.css
|
||||
external-in-external.css
|
||||
favicon.png
|
||||
image.gif
|
||||
inline.png
|
||||
script.js
|
2
tests/integration/data/downloads/mhtml/complex/script.js
Normal file
2
tests/integration/data/downloads/mhtml/complex/script.js
Normal file
@ -0,0 +1,2 @@
|
||||
function noop() {}
|
||||
noop();
|
10
tests/integration/data/downloads/mhtml/simple/simple.html
Normal file
10
tests/integration/data/downloads/mhtml/simple/simple.html
Normal file
@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Simple MHTML test</title>
|
||||
</head>
|
||||
<body>
|
||||
<a href="/">normal link to another page</a>
|
||||
</body>
|
||||
</html>
|
20
tests/integration/data/downloads/mhtml/simple/simple.mht
Normal file
20
tests/integration/data/downloads/mhtml/simple/simple.mht
Normal file
@ -0,0 +1,20 @@
|
||||
Content-Type: multipart/related; boundary="---=_qute-6d584056-b1e4-4882-91e6-d4a6d23adb67"
|
||||
MIME-Version: 1.0
|
||||
|
||||
-----=_qute-6d584056-b1e4-4882-91e6-d4a6d23adb67
|
||||
Content-Location: http://localhost:1234/data/downloads/mhtml/simple/simple.html
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/html; charset="UTF-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<!DOCTYPE=20html><html><head>
|
||||
=20=20=20=20=20=20=20=20<meta=20charset=3D"utf-8">
|
||||
=20=20=20=20=20=20=20=20<title>Simple=20MHTML=20test</title>
|
||||
=20=20=20=20</head>
|
||||
=20=20=20=20<body>
|
||||
=20=20=20=20=20=20=20=20<a=20href=3D"/">normal=20link=20to=20another=20page=
|
||||
</a>
|
||||
=20=20=20=20
|
||||
|
||||
</body></html>
|
||||
-----=_qute-6d584056-b1e4-4882-91e6-d4a6d23adb67--
|
@ -42,3 +42,28 @@ Feature: Downloading things from a website.
|
||||
Scenario: Retrying with no downloads
|
||||
When I run :download-retry
|
||||
Then the error "No failed downloads!" should be shown.
|
||||
|
||||
Scenario: :download with deprecated dest-old argument
|
||||
When I run :download http://localhost:(port)/ deprecated-argument
|
||||
Then the warning ":download [url] [dest] is deprecated - use download --dest [dest] [url]" should be shown.
|
||||
|
||||
Scenario: Two destinations given
|
||||
When I run :download --dest destination2 http://localhost:(port)/ destination1
|
||||
Then the warning ":download [url] [dest] is deprecated - use download --dest [dest] [url]" should be shown.
|
||||
And the error "Can't give two destinations for the download." should be shown.
|
||||
|
||||
Scenario: :download --mhtml with an URL given
|
||||
When I run :download --mhtml http://foobar/
|
||||
Then the error "Can only download the current page as mhtml." should be shown.
|
||||
|
||||
Scenario: Downloading as mhtml is available
|
||||
When I open html
|
||||
And I run :download --mhtml
|
||||
And I wait for "File successfully written." in the log
|
||||
Then no crash should happen
|
||||
|
||||
Scenario: Downloading as mhtml with non-ASCII headers
|
||||
When I open response-headers?Content-Type=text%2Fpl%C3%A4in
|
||||
And I run :download --mhtml --dest mhtml-response-headers.mht
|
||||
And I wait for "File successfully written." in the log
|
||||
Then no crash should happen
|
||||
|
@ -32,11 +32,11 @@ import tempfile
|
||||
|
||||
import yaml
|
||||
import pytest
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5.QtCore import pyqtSignal, QUrl
|
||||
|
||||
import testprocess # pylint: disable=import-error
|
||||
from qutebrowser.misc import ipc
|
||||
from qutebrowser.utils import log
|
||||
from qutebrowser.utils import log, utils
|
||||
|
||||
|
||||
def is_ignored_qt_message(message):
|
||||
@ -225,6 +225,9 @@ class QuteProc(testprocess.Process):
|
||||
def wait_for_load_finished(self, path, timeout=15000):
|
||||
"""Wait until any tab has finished loading."""
|
||||
url = self._path_to_url(path)
|
||||
# We really need the same representation that the webview uses in its
|
||||
# __repr__
|
||||
url = utils.elide(QUrl(url).toDisplayString(QUrl.EncodeUnicode), 100)
|
||||
pattern = re.compile(
|
||||
r"(load status for <qutebrowser.browser.webview.WebView "
|
||||
r"tab_id=\d+ url='{url}'>: LoadStatus.success|fetch: "
|
||||
|
116
tests/integration/test_mhtml_e2e.py
Normal file
116
tests/integration/test_mhtml_e2e.py
Normal file
@ -0,0 +1,116 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 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/>.
|
||||
|
||||
"""Test mhtml downloads based on sample files."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import os.path
|
||||
import collections
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def collect_tests():
|
||||
basedir = os.path.dirname(__file__)
|
||||
datadir = os.path.join(basedir, 'data', 'downloads', 'mhtml')
|
||||
files = os.listdir(datadir)
|
||||
return files
|
||||
|
||||
|
||||
def normalize_line(line):
|
||||
line = line.rstrip('\n')
|
||||
line = re.sub('boundary="---=_qute-[0-9a-f-]+"',
|
||||
'boundary="---=_qute-UUID"', line)
|
||||
line = re.sub('^-----=_qute-[0-9a-f-]+$', '-----=_qute-UUID', line)
|
||||
line = re.sub(r'localhost:\d{1,5}', 'localhost:(port)', line)
|
||||
|
||||
# Depending on Python's mimetypes module/the system's mime files, .js
|
||||
# files could be either identified as x-javascript or just javascript
|
||||
line = line.replace('Content-Type: application/x-javascript',
|
||||
'Content-Type: application/javascript')
|
||||
|
||||
return line
|
||||
|
||||
|
||||
class DownloadDir:
|
||||
|
||||
"""Abstraction over a download directory."""
|
||||
|
||||
def __init__(self, tmpdir):
|
||||
self._tmpdir = tmpdir
|
||||
self.location = str(tmpdir)
|
||||
|
||||
def read_file(self):
|
||||
files = self._tmpdir.listdir()
|
||||
assert len(files) == 1
|
||||
|
||||
with open(str(files[0]), 'r', encoding='utf-8') as f:
|
||||
return f.readlines()
|
||||
|
||||
def compare_mhtml(self, filename):
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
expected_data = [normalize_line(line) for line in f]
|
||||
actual_data = self.read_file()
|
||||
actual_data = [normalize_line(line) for line in actual_data]
|
||||
assert actual_data == expected_data
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def download_dir(tmpdir):
|
||||
return DownloadDir(tmpdir)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('test_name', collect_tests())
|
||||
def test_mhtml(test_name, download_dir, quteproc, httpbin):
|
||||
quteproc.set_setting('storage', 'download-directory',
|
||||
download_dir.location)
|
||||
quteproc.set_setting('storage', 'prompt-download-directory', 'false')
|
||||
|
||||
test_dir = os.path.join(os.path.abspath(os.path.dirname(__file__)),
|
||||
'data', 'downloads', 'mhtml', test_name)
|
||||
test_path = 'data/downloads/mhtml/{}'.format(test_name)
|
||||
|
||||
quteproc.open_path('{}/{}.html'.format(test_path, test_name))
|
||||
download_dest = os.path.join(download_dir.location,
|
||||
'{}-downloaded.mht'.format(test_name))
|
||||
# Discard all requests that were necessary to display the page
|
||||
httpbin.clear_data()
|
||||
quteproc.send_cmd(':download --mhtml --dest "{}"'.format(download_dest))
|
||||
quteproc.wait_for(category='downloads', module='mhtml',
|
||||
function='_finish_file',
|
||||
message='File successfully written.')
|
||||
|
||||
expected_file = os.path.join(test_dir, '{}.mht'.format(test_name))
|
||||
download_dir.compare_mhtml(expected_file)
|
||||
|
||||
with open(os.path.join(test_dir, 'requests'), encoding='utf-8') as f:
|
||||
expected_requests = []
|
||||
for line in f:
|
||||
if line.startswith('#'):
|
||||
continue
|
||||
path = '/{}/{}'.format(test_path, line.strip())
|
||||
expected_requests.append(httpbin.ExpectedRequest('GET', path))
|
||||
|
||||
actual_requests = httpbin.get_requests()
|
||||
# Requests are not hashable, we need to convert to ExpectedRequests
|
||||
actual_requests = map(httpbin.ExpectedRequest.from_request,
|
||||
actual_requests)
|
||||
assert (collections.Counter(actual_requests) ==
|
||||
collections.Counter(expected_requests))
|
@ -182,13 +182,17 @@ class Process(QObject):
|
||||
time.sleep(1)
|
||||
# Exit the process to make sure we're in a defined state again
|
||||
self.terminate()
|
||||
self._data.clear()
|
||||
self.clear_data()
|
||||
raise InvalidLine(self._invalid)
|
||||
|
||||
self._data.clear()
|
||||
self.clear_data()
|
||||
if not self.is_running():
|
||||
raise ProcessExited
|
||||
|
||||
def clear_data(self):
|
||||
"""Clear the collected data."""
|
||||
self._data.clear()
|
||||
|
||||
def terminate(self):
|
||||
"""Clean up and shut down the process."""
|
||||
self.proc.terminate()
|
||||
|
@ -97,6 +97,11 @@ class ExpectedRequest:
|
||||
self.verb = verb
|
||||
self.path = path
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, request):
|
||||
"""Create an ExpectedRequest from a Request."""
|
||||
return cls(request.verb, request.path)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, (Request, ExpectedRequest)):
|
||||
return (self.verb == other.verb and
|
||||
@ -104,6 +109,13 @@ class ExpectedRequest:
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __hash__(self):
|
||||
return hash(('ExpectedRequest', self.verb, self.path))
|
||||
|
||||
def __repr__(self):
|
||||
return ('ExpectedRequest(verb={!r}, path={!r})'
|
||||
.format(self.verb, self.path))
|
||||
|
||||
|
||||
class HTTPBin(testprocess.Process):
|
||||
|
||||
|
@ -24,10 +24,13 @@ This script gets called as a QProcess from integration/conftest.py.
|
||||
|
||||
import sys
|
||||
import time
|
||||
import os.path
|
||||
import signal
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from httpbin.core import app
|
||||
from httpbin.structures import CaseInsensitiveDict
|
||||
import cherrypy.wsgiserver
|
||||
import flask
|
||||
|
||||
|
||||
@ -51,11 +54,63 @@ def redirect_later():
|
||||
return flask.redirect('/')
|
||||
|
||||
|
||||
@app.after_request
|
||||
def log_request(response):
|
||||
request = flask.request
|
||||
template = '127.0.0.1 - - [{date}] "{verb} {path} {http}" {status} -'
|
||||
print(template.format(
|
||||
date=datetime.now().strftime('%d/%b/%Y %H:%M:%S'),
|
||||
verb=request.method,
|
||||
path=request.full_path if request.query_string else request.path,
|
||||
http=request.environ['SERVER_PROTOCOL'],
|
||||
status=response.status_code,
|
||||
), file=sys.stderr, flush=True)
|
||||
return response
|
||||
|
||||
|
||||
class WSGIServer(cherrypy.wsgiserver.CherryPyWSGIServer):
|
||||
|
||||
"""A custom WSGIServer that prints a line on stderr when it's ready."""
|
||||
|
||||
# pylint: disable=no-member
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issues/702
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._ready = False
|
||||
self._printed_ready = False
|
||||
|
||||
@property
|
||||
def ready(self):
|
||||
return self._ready
|
||||
|
||||
@ready.setter
|
||||
def ready(self, value):
|
||||
if value and not self._printed_ready:
|
||||
print(' * Running on http://127.0.0.1:{}/ (Press CTRL+C to quit)'
|
||||
.format(self.bind_addr[1]), file=sys.stderr, flush=True)
|
||||
self._printed_ready = True
|
||||
self._ready = value
|
||||
|
||||
|
||||
def main():
|
||||
# pylint: disable=no-member
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issues/702
|
||||
# "Instance of 'WSGIServer' has no 'start' member (no-member)"
|
||||
# "Instance of 'WSGIServer' has no 'stop' member (no-member)"
|
||||
|
||||
if hasattr(sys, 'frozen'):
|
||||
basedir = os.path.realpath(os.path.dirname(sys.executable))
|
||||
app.template_folder = os.path.join(basedir, 'integration', 'templates')
|
||||
app.run(port=int(sys.argv[1]), debug=True, use_reloader=False)
|
||||
port = int(sys.argv[1])
|
||||
server = WSGIServer(('127.0.0.1', port), app)
|
||||
|
||||
signal.signal(signal.SIGTERM, lambda *args: server.stop())
|
||||
|
||||
try:
|
||||
server.start()
|
||||
except KeyboardInterrupt:
|
||||
server.stop()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
298
tests/unit/browser/test_mhtml.py
Normal file
298
tests/unit/browser/test_mhtml.py
Normal file
@ -0,0 +1,298 @@
|
||||
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
|
||||
|
||||
# Copyright 2015 Daniel Schadt
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
"""Tests for qutebrowser.browser.mhtml."""
|
||||
|
||||
import io
|
||||
import textwrap
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from qutebrowser.browser import mhtml
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def patch_uuid(monkeypatch):
|
||||
monkeypatch.setattr("uuid.uuid4", lambda: "UUID")
|
||||
|
||||
|
||||
class Checker:
|
||||
|
||||
"""A helper to check mhtml output.
|
||||
|
||||
Attrs:
|
||||
fp: A BytesIO object for passing to MHTMLWriter.write_to.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.fp = io.BytesIO()
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.fp.getvalue()
|
||||
|
||||
def expect(self, expected):
|
||||
actual = self.value.decode('ascii')
|
||||
# Make sure there are no stray \r or \n
|
||||
assert re.search(r'\r[^\n]', actual) is None
|
||||
assert re.search(r'[^\r]\n', actual) is None
|
||||
actual = actual.replace('\r\n', '\n')
|
||||
expected = textwrap.dedent(expected).lstrip('\n')
|
||||
assert expected == actual
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def checker():
|
||||
return Checker()
|
||||
|
||||
|
||||
def test_quoted_printable_umlauts(checker):
|
||||
content = 'Die süße Hündin läuft in die Höhle des Bären'
|
||||
content = content.encode('iso-8859-1')
|
||||
writer = mhtml.MHTMLWriter(root_content=content,
|
||||
content_location='localhost',
|
||||
content_type='text/plain')
|
||||
writer.write_to(checker.fp)
|
||||
checker.expect("""
|
||||
Content-Type: multipart/related; boundary="---=_qute-UUID"
|
||||
MIME-Version: 1.0
|
||||
|
||||
-----=_qute-UUID
|
||||
Content-Location: localhost
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Die=20s=FC=DFe=20H=FCndin=20l=E4uft=20in=20die=20H=F6hle=20des=20B=E4ren
|
||||
-----=_qute-UUID--
|
||||
""")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('header, value', [
|
||||
('content_location', 'http://brötli.com'),
|
||||
('content_type', 'text/pläin'),
|
||||
])
|
||||
def test_refuses_non_ascii_header_value(checker, header, value):
|
||||
defaults = {
|
||||
'root_content': b'',
|
||||
'content_location': 'http://example.com',
|
||||
'content_type': 'text/plain',
|
||||
}
|
||||
defaults[header] = value
|
||||
writer = mhtml.MHTMLWriter(**defaults)
|
||||
with pytest.raises(UnicodeEncodeError) as excinfo:
|
||||
writer.write_to(checker.fp)
|
||||
assert "'ascii' codec can't encode" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_file_encoded_as_base64(checker):
|
||||
content = b'Image file attached'
|
||||
writer = mhtml.MHTMLWriter(root_content=content, content_type='text/plain',
|
||||
content_location='http://example.com')
|
||||
writer.add_file(location='http://a.example.com/image.png',
|
||||
content='\U0001F601 image data'.encode('utf-8'),
|
||||
content_type='image/png',
|
||||
transfer_encoding=mhtml.E_BASE64)
|
||||
writer.write_to(checker.fp)
|
||||
checker.expect("""
|
||||
Content-Type: multipart/related; boundary="---=_qute-UUID"
|
||||
MIME-Version: 1.0
|
||||
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://example.com
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
Image=20file=20attached
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://a.example.com/image.png
|
||||
MIME-Version: 1.0
|
||||
Content-Type: image/png
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
8J+YgSBpbWFnZSBkYXRh
|
||||
|
||||
-----=_qute-UUID--
|
||||
""")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('transfer_encoding', [mhtml.E_BASE64, mhtml.E_QUOPRI],
|
||||
ids=['base64', 'quoted-printable'])
|
||||
def test_payload_lines_wrap(checker, transfer_encoding):
|
||||
payload = b'1234567890' * 10
|
||||
writer = mhtml.MHTMLWriter(root_content=b'', content_type='text/plain',
|
||||
content_location='http://example.com')
|
||||
writer.add_file(location='http://example.com/payload', content=payload,
|
||||
content_type='text/plain',
|
||||
transfer_encoding=transfer_encoding)
|
||||
writer.write_to(checker.fp)
|
||||
for line in checker.value.split(b'\r\n'):
|
||||
assert len(line) < 77
|
||||
|
||||
|
||||
def test_files_appear_sorted(checker):
|
||||
writer = mhtml.MHTMLWriter(root_content=b'root file',
|
||||
content_type='text/plain',
|
||||
content_location='http://www.example.com/')
|
||||
for subdomain in 'ahgbizt':
|
||||
writer.add_file(location='http://{}.example.com/'.format(subdomain),
|
||||
content='file {}'.format(subdomain).encode('utf-8'),
|
||||
content_type='text/plain',
|
||||
transfer_encoding=mhtml.E_QUOPRI)
|
||||
writer.write_to(checker.fp)
|
||||
checker.expect("""
|
||||
Content-Type: multipart/related; boundary="---=_qute-UUID"
|
||||
MIME-Version: 1.0
|
||||
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://www.example.com/
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
root=20file
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://a.example.com/
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
file=20a
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://b.example.com/
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
file=20b
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://g.example.com/
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
file=20g
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://h.example.com/
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
file=20h
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://i.example.com/
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
file=20i
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://t.example.com/
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
file=20t
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://z.example.com/
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
file=20z
|
||||
-----=_qute-UUID--
|
||||
""")
|
||||
|
||||
|
||||
def test_empty_content_type(checker):
|
||||
writer = mhtml.MHTMLWriter(root_content=b'',
|
||||
content_location='http://example.com/',
|
||||
content_type='text/plain')
|
||||
writer.add_file('http://example.com/file', b'file content')
|
||||
writer.write_to(checker.fp)
|
||||
checker.expect("""
|
||||
Content-Type: multipart/related; boundary="---=_qute-UUID"
|
||||
MIME-Version: 1.0
|
||||
|
||||
-----=_qute-UUID
|
||||
Content-Location: http://example.com/
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
|
||||
-----=_qute-UUID
|
||||
MIME-Version: 1.0
|
||||
Content-Location: http://example.com/file
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
file=20content
|
||||
-----=_qute-UUID--
|
||||
""")
|
||||
|
||||
|
||||
@pytest.mark.parametrize('has_cssutils', [
|
||||
pytest.mark.skipif(mhtml.cssutils is None,
|
||||
reason="requires cssutils")(True),
|
||||
False,
|
||||
], ids=['with_cssutils', 'no_cssutils'])
|
||||
@pytest.mark.parametrize('inline, style, expected_urls', [
|
||||
(False, "@import 'default.css'", ['default.css']),
|
||||
(False, '@import "default.css"', ['default.css']),
|
||||
(False, "@import \t 'tabbed.css'", ['tabbed.css']),
|
||||
(False, "@import url('default.css')", ['default.css']),
|
||||
(False, """body {
|
||||
background: url("/bg-img.png")
|
||||
}""", ['/bg-img.png']),
|
||||
(True, 'background: url(folder/file.png) no-repeat', ['folder/file.png']),
|
||||
(True, 'content: url()', []),
|
||||
])
|
||||
def test_css_url_scanner(monkeypatch, has_cssutils, inline, style,
|
||||
expected_urls):
|
||||
if not has_cssutils:
|
||||
monkeypatch.setattr('qutebrowser.browser.mhtml.cssutils', None)
|
||||
expected_urls.sort()
|
||||
urls = mhtml._get_css_imports(style, inline=inline)
|
||||
urls.sort()
|
||||
assert urls == expected_urls
|
||||
|
||||
|
||||
class TestNoCloseBytesIO:
|
||||
# WORKAROUND for https://bitbucket.org/logilab/pylint/issues/540/
|
||||
# pylint: disable=no-member
|
||||
|
||||
def test_fake_close(self):
|
||||
fp = mhtml._NoCloseBytesIO()
|
||||
fp.write(b'Value')
|
||||
fp.close()
|
||||
assert fp.getvalue() == b'Value'
|
||||
fp.write(b'Eulav')
|
||||
assert fp.getvalue() == b'ValueEulav'
|
||||
|
||||
def test_actual_close(self):
|
||||
fp = mhtml._NoCloseBytesIO()
|
||||
fp.write(b'Value')
|
||||
fp.actual_close()
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
fp.getvalue()
|
||||
assert str(excinfo.value) == 'I/O operation on closed file.'
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
fp.write(b'Closed')
|
||||
assert str(excinfo.value) == 'I/O operation on closed file.'
|
@ -527,6 +527,19 @@ def test_same_domain_invalid_url(url1, url2):
|
||||
with pytest.raises(urlutils.InvalidUrlError):
|
||||
urlutils.same_domain(QUrl(url1), QUrl(url2))
|
||||
|
||||
|
||||
@pytest.mark.parametrize('url, expected', [
|
||||
('http://example.com', 'http://example.com'),
|
||||
('http://ünicode.com', 'http://xn--nicode-2ya.com'),
|
||||
('http://foo.bar/?header=text/pläin',
|
||||
'http://foo.bar/?header=text/pl%C3%A4in'),
|
||||
])
|
||||
def test_encoded_url(url, expected):
|
||||
"""Test encoded_url"""
|
||||
url = QUrl(url)
|
||||
assert urlutils.encoded_url(url) == expected
|
||||
|
||||
|
||||
class TestIncDecNumber:
|
||||
|
||||
"""Tests for urlutils.incdec_number()."""
|
||||
|
@ -880,6 +880,20 @@ def test_force_encoding(inp, enc, expected):
|
||||
assert utils.force_encoding(inp, enc) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize('inp, expected', [
|
||||
('normal.txt', 'normal.txt'),
|
||||
('user/repo issues.mht', 'user_repo issues.mht'),
|
||||
('<Test\\File> - "*?:|', '_Test_File_ - _____'),
|
||||
])
|
||||
def test_sanitize_filename(inp, expected):
|
||||
assert utils.sanitize_filename(inp) == expected
|
||||
|
||||
|
||||
def test_sanitize_filename_empty_replacement():
|
||||
name = '/<Bad File>/'
|
||||
assert utils.sanitize_filename(name, replacement=None) == 'Bad File'
|
||||
|
||||
|
||||
class TestNewestSlice:
|
||||
|
||||
"""Test newest_slice."""
|
||||
|
@ -324,6 +324,7 @@ class ImportFake:
|
||||
'jinja2': True,
|
||||
'pygments': True,
|
||||
'yaml': True,
|
||||
'cssutils': True,
|
||||
}
|
||||
self.version_attribute = '__version__'
|
||||
self.version = '1.2.3'
|
||||
@ -383,12 +384,13 @@ class TestModuleVersions:
|
||||
"""Test with all modules present in version 1.2.3."""
|
||||
expected = ['sip: yes', 'colorlog: yes', 'colorama: 1.2.3',
|
||||
'pypeg2: 1.2.3', 'jinja2: 1.2.3', 'pygments: 1.2.3',
|
||||
'yaml: 1.2.3']
|
||||
'yaml: 1.2.3', 'cssutils: 1.2.3']
|
||||
assert version._module_versions() == expected
|
||||
|
||||
@pytest.mark.parametrize('module, idx, expected', [
|
||||
('colorlog', 1, 'colorlog: no'),
|
||||
('colorama', 2, 'colorama: no'),
|
||||
('cssutils', 7, 'cssutils: no'),
|
||||
])
|
||||
def test_missing_module(self, module, idx, expected, import_fake):
|
||||
"""Test with a module missing.
|
||||
@ -404,12 +406,13 @@ class TestModuleVersions:
|
||||
@pytest.mark.parametrize('value, expected', [
|
||||
('VERSION', ['sip: yes', 'colorlog: yes', 'colorama: 1.2.3',
|
||||
'pypeg2: yes', 'jinja2: yes', 'pygments: yes',
|
||||
'yaml: yes']),
|
||||
'yaml: yes', 'cssutils: yes']),
|
||||
('SIP_VERSION_STR', ['sip: 1.2.3', 'colorlog: yes', 'colorama: yes',
|
||||
'pypeg2: yes', 'jinja2: yes', 'pygments: yes',
|
||||
'yaml: yes']),
|
||||
'yaml: yes', 'cssutils: yes']),
|
||||
(None, ['sip: yes', 'colorlog: yes', 'colorama: yes', 'pypeg2: yes',
|
||||
'jinja2: yes', 'pygments: yes', 'yaml: yes']),
|
||||
'jinja2: yes', 'pygments: yes', 'yaml: yes',
|
||||
'cssutils: yes']),
|
||||
])
|
||||
def test_version_attribute(self, value, expected, import_fake):
|
||||
"""Test with a different version attribute.
|
||||
@ -432,6 +435,7 @@ class TestModuleVersions:
|
||||
('jinja2', True),
|
||||
('pygments', True),
|
||||
('yaml', True),
|
||||
('cssutils', True),
|
||||
])
|
||||
def test_existing_attributes(self, name, has_version):
|
||||
"""Check if all dependencies have an expected __version__ attribute.
|
||||
|
1
tox.ini
1
tox.ini
@ -42,6 +42,7 @@ deps =
|
||||
Werkzeug==0.11.2
|
||||
wheel==0.26.0
|
||||
xvfbwrapper==0.2.5
|
||||
cherrypy==3.8.0
|
||||
commands =
|
||||
{envpython} scripts/link_pyqt.py --tox {envdir}
|
||||
{envpython} -m py.test --strict -rfEsw --faulthandler-timeout=70 --cov --cov-report xml --cov-report=html --cov-report= {posargs:tests}
|
||||
|
Loading…
Reference in New Issue
Block a user