Merge branch 'Kingdread-issue-2204'

This commit is contained in:
Florian Bruhin 2017-02-04 18:43:00 +01:00
commit 876414565c
8 changed files with 181 additions and 76 deletions

View File

@ -39,6 +39,7 @@ Fixed
- `:enter-mode` now refuses to enter modes which can't be entered manually (which caused crashes). - `:enter-mode` now refuses to enter modes which can't be entered manually (which caused crashes).
- `:record-macro` (`q`) now doesn't try to record macros for special keys without a text. - `:record-macro` (`q`) now doesn't try to record macros for special keys without a text.
- Fixed PAC (proxy autoconfig) not working with QtWebKit - Fixed PAC (proxy autoconfig) not working with QtWebKit
- `:download --mhtml` now uses the new file dialog
v0.9.1 v0.9.1
------ ------

View File

@ -1302,54 +1302,50 @@ class CommandDispatcher:
# FIXME:qtwebengine do this with the QtWebEngine download manager? # FIXME:qtwebengine do this with the QtWebEngine download manager?
download_manager = objreg.get('qtnetwork-download-manager', download_manager = objreg.get('qtnetwork-download-manager',
scope='window', window=self._win_id) scope='window', window=self._win_id)
target = None
if dest is not None:
target = downloads.FileDownloadTarget(dest)
if url: if url:
if mhtml_: if mhtml_:
raise cmdexc.CommandError("Can only download the current page" raise cmdexc.CommandError("Can only download the current page"
" as mhtml.") " as mhtml.")
url = urlutils.qurl_from_user_input(url) url = urlutils.qurl_from_user_input(url)
urlutils.raise_cmdexc_if_invalid(url) urlutils.raise_cmdexc_if_invalid(url)
if dest is None:
target = None
else:
target = downloads.FileDownloadTarget(dest)
download_manager.get(url, target=target) download_manager.get(url, target=target)
elif mhtml_: elif mhtml_:
self._download_mhtml(dest) self._download_mhtml(target)
else: else:
qnam = self._current_widget().networkaccessmanager() qnam = self._current_widget().networkaccessmanager()
if dest is None:
target = None
else:
target = downloads.FileDownloadTarget(dest)
download_manager.get(self._current_url(), qnam=qnam, target=target) download_manager.get(self._current_url(), qnam=qnam, target=target)
def _download_mhtml(self, dest=None): def _download_mhtml(self, target=None):
"""Download the current page as an MHTML file, including all assets. """Download the current page as an MHTML file, including all assets.
Args: Args:
dest: The file path to write the download to. target: The download target for the file.
""" """
tab = self._current_widget() tab = self._current_widget()
if tab.backend == usertypes.Backend.QtWebEngine: if tab.backend == usertypes.Backend.QtWebEngine:
raise cmdexc.CommandError("Download --mhtml is not implemented " raise cmdexc.CommandError("Download --mhtml is not implemented "
"with QtWebEngine yet") "with QtWebEngine yet")
if target is not None:
mhtml.start_download_checked(target, tab=tab)
return
if dest is None: suggested_fn = self._current_title() + ".mht"
suggested_fn = self._current_title() + ".mht" suggested_fn = utils.sanitize_filename(suggested_fn)
suggested_fn = utils.sanitize_filename(suggested_fn)
filename = downloads.immediate_download_path() filename = downloads.immediate_download_path()
if filename is not None: if filename is not None:
mhtml.start_download_checked(filename, tab=tab) target = downloads.FileDownloadTarget(filename)
else: mhtml.start_download_checked(target, tab=tab)
question = downloads.get_filename_question(
suggested_filename=suggested_fn, url=tab.url(), parent=tab)
question.answered.connect(functools.partial(
mhtml.start_download_checked, tab=tab))
message.global_bridge.ask(question, blocking=False)
else: else:
mhtml.start_download_checked(dest, tab=tab) question = downloads.get_filename_question(
suggested_filename=suggested_fn, url=tab.url(), parent=tab)
question.answered.connect(functools.partial(
mhtml.start_download_checked, tab=tab))
message.global_bridge.ask(question, blocking=False)
@cmdutils.register(instance='command-dispatcher', scope='window') @cmdutils.register(instance='command-dispatcher', scope='window')
def view_source(self): def view_source(self):

View File

@ -20,7 +20,6 @@
"""Shared QtWebKit/QtWebEngine code for downloads.""" """Shared QtWebKit/QtWebEngine code for downloads."""
import sys import sys
import shlex
import html import html
import os.path import os.path
import collections import collections
@ -28,15 +27,13 @@ import functools
import tempfile import tempfile
import sip import sip
from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QUrl, QModelIndex, from PyQt5.QtCore import (pyqtSlot, pyqtSignal, Qt, QObject, QModelIndex,
QTimer, QAbstractListModel) QTimer, QAbstractListModel)
from PyQt5.QtGui import QDesktopServices
from qutebrowser.commands import cmdexc, cmdutils from qutebrowser.commands import cmdexc, cmdutils
from qutebrowser.config import config from qutebrowser.config import config
from qutebrowser.utils import (usertypes, standarddir, utils, message, log, from qutebrowser.utils import (usertypes, standarddir, utils, message, log,
qtutils) qtutils)
from qutebrowser.misc import guiprocess
ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole, ModelRole = usertypes.enum('ModelRole', ['item'], start=Qt.UserRole,
@ -158,7 +155,7 @@ def get_filename_question(*, suggested_filename, url, parent=None):
q.title = "Save file to:" q.title = "Save file to:"
q.text = "Please enter a location for <b>{}</b>".format( q.text = "Please enter a location for <b>{}</b>".format(
html.escape(url.toDisplayString())) html.escape(url.toDisplayString()))
q.mode = usertypes.PromptMode.text q.mode = usertypes.PromptMode.download
q.completed.connect(q.deleteLater) q.completed.connect(q.deleteLater)
q.default = _path_suggestion(suggested_filename) q.default = _path_suggestion(suggested_filename)
return q return q
@ -197,6 +194,9 @@ class FileDownloadTarget(_DownloadTarget):
def suggested_filename(self): def suggested_filename(self):
return os.path.basename(self.filename) return os.path.basename(self.filename)
def __str__(self):
return self.filename
class FileObjDownloadTarget(_DownloadTarget): class FileObjDownloadTarget(_DownloadTarget):
@ -216,6 +216,12 @@ class FileObjDownloadTarget(_DownloadTarget):
except AttributeError: except AttributeError:
raise NoFilenameError raise NoFilenameError
def __str__(self):
try:
return 'file object at {}'.format(self.fileobj.name)
except AttributeError:
return 'anonymous file object'
class OpenFileDownloadTarget(_DownloadTarget): class OpenFileDownloadTarget(_DownloadTarget):
@ -234,6 +240,9 @@ class OpenFileDownloadTarget(_DownloadTarget):
def suggested_filename(self): def suggested_filename(self):
raise NoFilenameError raise NoFilenameError
def __str__(self):
return 'temporary file'
class DownloadItemStats(QObject): class DownloadItemStats(QObject):
@ -520,31 +529,7 @@ class AbstractDownloadItem(QObject):
if filename is None: # pragma: no cover if filename is None: # pragma: no cover
log.downloads.error("No filename to open the download!") log.downloads.error("No filename to open the download!")
return return
utils.open_file(filename, cmdline)
# the default program to open downloads with - will be empty string
# if we want to use the default
override = config.get('general', 'default-open-dispatcher')
# precedence order: cmdline > default-open-dispatcher > openUrl
if cmdline is None and not override:
log.downloads.debug("Opening {} with the system application"
.format(filename))
url = QUrl.fromLocalFile(filename)
QDesktopServices.openUrl(url)
return
if cmdline is None and override:
cmdline = override
cmd, *args = shlex.split(cmdline)
args = [arg.replace('{}', filename) for arg in args]
if '{}' not in cmdline:
args.append(filename)
log.downloads.debug("Opening {} with {}"
.format(filename, [cmd] + args))
proc = guiprocess.GUIProcess(what='download')
proc.start_detached(cmd, args)
def _ensure_can_set_filename(self, filename): def _ensure_can_set_filename(self, filename):
"""Make sure we can still set a filename.""" """Make sure we can still set a filename."""
@ -780,7 +765,6 @@ class AbstractDownloadManager(QObject):
def _init_filename_question(self, question, download): def _init_filename_question(self, question, download):
"""Set up an existing filename question with a download.""" """Set up an existing filename question with a download."""
question.mode = usertypes.PromptMode.download
question.answered.connect(download.set_target) question.answered.connect(download.set_target)
question.cancelled.connect(download.cancel) question.cancelled.connect(download.cancel)
download.cancelled.connect(question.abort) download.cancelled.connect(question.abort)

View File

@ -242,7 +242,7 @@ class _Downloader:
Attributes: Attributes:
tab: The AbstractTab which contains the website that will be saved. tab: The AbstractTab which contains the website that will be saved.
dest: Destination filename. target: DownloadTarget where the file should be downloaded to.
writer: The MHTMLWriter object which is used to save the page. writer: The MHTMLWriter object which is used to save the page.
loaded_urls: A set of QUrls of finished asset downloads. loaded_urls: A set of QUrls of finished asset downloads.
pending_downloads: A set of unfinished (url, DownloadItem) tuples. pending_downloads: A set of unfinished (url, DownloadItem) tuples.
@ -252,9 +252,9 @@ class _Downloader:
_win_id: The window this downloader belongs to. _win_id: The window this downloader belongs to.
""" """
def __init__(self, tab, dest): def __init__(self, tab, target):
self.tab = tab self.tab = tab
self.dest = dest self.target = target
self.writer = None self.writer = None
self.loaded_urls = {tab.url()} self.loaded_urls = {tab.url()}
self.pending_downloads = set() self.pending_downloads = set()
@ -462,14 +462,34 @@ class _Downloader:
return return
self._finished_file = True self._finished_file = True
log.downloads.debug("All assets downloaded, ready to finish off!") log.downloads.debug("All assets downloaded, ready to finish off!")
if isinstance(self.target, downloads.FileDownloadTarget):
fobj = open(self.target.filename, 'wb')
elif isinstance(self.target, downloads.FileObjDownloadTarget):
fobj = self.target.fileobj
elif isinstance(self.target, downloads.OpenFileDownloadTarget):
try:
fobj = downloads.temp_download_manager.get_tmpfile(
self.tab.title() + '.mht')
except OSError as exc:
msg = "Download error: {}".format(exc)
message.error(msg)
return
else:
raise ValueError("Invalid DownloadTarget given: {!r}"
.format(self.target))
try: try:
with open(self.dest, 'wb') as file_output: with fobj:
self.writer.write_to(file_output) self.writer.write_to(fobj)
except OSError as error: except OSError as error:
message.error("Could not save file: {}".format(error)) message.error("Could not save file: {}".format(error))
return return
log.downloads.debug("File successfully written.") log.downloads.debug("File successfully written.")
message.info("Page saved as {}".format(self.dest)) message.info("Page saved as {}".format(self.target))
if isinstance(self.target, downloads.OpenFileDownloadTarget):
utils.open_file(fobj.name, self.target.cmdline)
def _collect_zombies(self): def _collect_zombies(self):
"""Collect done downloads and add their data to the MHTML file. """Collect done downloads and add their data to the MHTML file.
@ -501,26 +521,29 @@ class _NoCloseBytesIO(io.BytesIO):
super().close() super().close()
def _start_download(dest, tab): def _start_download(target, tab):
"""Start downloading the current page and all assets to an MHTML file. """Start downloading the current page and all assets to an MHTML file.
This will overwrite dest if it already exists. This will overwrite dest if it already exists.
Args: Args:
dest: The filename where the resulting file should be saved. target: The DownloadTarget where the resulting file should be saved.
tab: Specify the tab whose page should be loaded. tab: Specify the tab whose page should be loaded.
""" """
loader = _Downloader(tab, dest) loader = _Downloader(tab, target)
loader.run() loader.run()
def start_download_checked(dest, tab): def start_download_checked(target, tab):
"""First check if dest is already a file, then start the download. """First check if dest is already a file, then start the download.
Args: Args:
dest: The filename where the resulting file should be saved. target: The DownloadTarget where the resulting file should be saved.
tab: Specify the tab whose page should be loaded. tab: Specify the tab whose page should be loaded.
""" """
if not isinstance(target, downloads.FileDownloadTarget):
_start_download(target, tab)
return
# The default name is 'page title.mht' # The default name is 'page title.mht'
title = tab.title() title = tab.title()
default_name = utils.sanitize_filename(title + '.mht') default_name = utils.sanitize_filename(title + '.mht')
@ -528,7 +551,7 @@ def start_download_checked(dest, tab):
# Remove characters which cannot be expressed in the file system encoding # Remove characters which cannot be expressed in the file system encoding
encoding = sys.getfilesystemencoding() encoding = sys.getfilesystemencoding()
default_name = utils.force_encoding(default_name, encoding) default_name = utils.force_encoding(default_name, encoding)
dest = utils.force_encoding(dest, encoding) dest = utils.force_encoding(target.filename, encoding)
dest = os.path.expanduser(dest) dest = os.path.expanduser(dest)
@ -549,8 +572,9 @@ def start_download_checked(dest, tab):
message.error("Directory {} does not exist.".format(folder)) message.error("Directory {} does not exist.".format(folder))
return return
target = downloads.FileDownloadTarget(path)
if not os.path.isfile(path): if not os.path.isfile(path):
_start_download(path, tab=tab) _start_download(target, tab=tab)
return return
q = usertypes.Question() q = usertypes.Question()
@ -560,5 +584,5 @@ def start_download_checked(dest, tab):
html.escape(path)) html.escape(path))
q.completed.connect(q.deleteLater) q.completed.connect(q.deleteLater)
q.answered_yes.connect(functools.partial( q.answered_yes.connect(functools.partial(
_start_download, path, tab=tab)) _start_download, target, tab=tab))
message.global_bridge.ask(q, blocking=False) message.global_bridge.ask(q, blocking=False)

View File

@ -29,9 +29,10 @@ import functools
import contextlib import contextlib
import itertools import itertools
import socket import socket
import shlex
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QKeySequence, QColor, QClipboard from PyQt5.QtGui import QKeySequence, QColor, QClipboard, QDesktopServices
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
import pkg_resources import pkg_resources
@ -825,3 +826,48 @@ def random_port():
port = sock.getsockname()[1] port = sock.getsockname()[1]
sock.close() sock.close()
return port return port
def open_file(filename, cmdline=None):
"""Open the given file.
If cmdline is not given, general->default-open-dispatcher is used.
If default-open-dispatcher is unset, the system's default application is
used.
Args:
filename: The filename to open.
cmdline: The command to use as string. A `{}` is expanded to the
filename. None means to use the system's default application
or `default-open-dispatcher` if set. If no `{}` is found, the
filename is appended to the cmdline.
"""
# Import late to avoid circular imports:
# utils -> config -> configdata -> configtypes -> cmdutils -> command ->
# utils
from qutebrowser.misc import guiprocess
from qutebrowser.config import config
# the default program to open downloads with - will be empty string
# if we want to use the default
override = config.get('general', 'default-open-dispatcher')
# precedence order: cmdline > default-open-dispatcher > openUrl
if cmdline is None and not override:
log.misc.debug("Opening {} with the system application"
.format(filename))
url = QUrl.fromLocalFile(filename)
QDesktopServices.openUrl(url)
return
if cmdline is None and override:
cmdline = override
cmd, *args = shlex.split(cmdline)
args = [arg.replace('{}', filename) for arg in args]
if '{}' not in cmdline:
args.append(filename)
log.misc.debug("Opening {} with {}"
.format(filename, [cmd] + args))
proc = guiprocess.GUIProcess(what='open-file')
proc.start_detached(cmd, args)

View File

@ -216,17 +216,26 @@ Feature: Downloading things from a website.
When I set storage -> prompt-download-directory to true When I set storage -> prompt-download-directory to true
And I open html And I open html
And I run :download --mhtml And I run :download --mhtml
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.text: 2> text='Please enter a location for <b>http://localhost:*/html</b>' title='Save file to:'>, *" in the log And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> text='Please enter a location for <b>http://localhost:*/html</b>' title='Save file to:'>, *" in the log
And I run :prompt-accept And I run :prompt-accept
And I wait for "File successfully written." in the log And I wait for "File successfully written." in the log
And I run :download --mhtml And I run :download --mhtml
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.text: 2> text='Please enter a location for <b>http://localhost:*/html</b>' title='Save file to:'>, *" in the log And I wait for "Asking question <qutebrowser.utils.usertypes.Question default='*' mode=<PromptMode.download: 5> text='Please enter a location for <b>http://localhost:*/html</b>' title='Save file to:'>, *" in the log
And I run :prompt-accept And I run :prompt-accept
And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=None mode=<PromptMode.yesno: 1> text='<b>*</b> already exists. Overwrite?' title='Overwrite existing file?'>, *" in the log And I wait for "Asking question <qutebrowser.utils.usertypes.Question default=None mode=<PromptMode.yesno: 1> text='<b>*</b> already exists. Overwrite?' title='Overwrite existing file?'>, *" in the log
And I run :prompt-accept yes And I run :prompt-accept yes
And I wait for "File successfully written." in the log And I wait for "File successfully written." in the log
Then no crash should happen Then no crash should happen
@qtwebengine_todo: :download --mhtml is not implemented yet
Scenario: Opening a mhtml download directly
When I set storage -> prompt-download-directory to true
And I open html
And I run :download --mhtml
And I wait for the download prompt for "*"
And I directly open the download
Then "Opening *.mht* with [*python*]" should be logged
## :download-cancel ## :download-cancel
Scenario: Cancelling a download Scenario: Cancelling a download

View File

@ -96,7 +96,7 @@ def test_ascii_locale(request, httpbin, tmpdir, quteproc_new):
.format(sys.executable)) .format(sys.executable))
quteproc_new.wait_for(category='downloads', quteproc_new.wait_for(category='downloads',
message='Download ä-issue908.bin finished') message='Download ä-issue908.bin finished')
quteproc_new.wait_for(category='downloads', quteproc_new.wait_for(category='misc',
message='Opening * with [*python*]') message='Opening * with [*python*]')
assert len(tmpdir.listdir()) == 1 assert len(tmpdir.listdir()) == 1

View File

@ -27,8 +27,10 @@ import logging
import functools import functools
import collections import collections
import socket import socket
import re
import shlex
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QColor, QClipboard from PyQt5.QtGui import QColor, QClipboard
import pytest import pytest
@ -984,3 +986,46 @@ def test_random_port():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('localhost', port)) sock.bind(('localhost', port))
sock.close() sock.close()
class TestOpenFile:
@pytest.mark.not_frozen
def test_cmdline_without_argument(self, caplog, config_stub):
config_stub.data = {'general': {'default-open-dispatcher': ''}}
executable = shlex.quote(sys.executable)
cmdline = '{} -c pass'.format(executable)
utils.open_file('/foo/bar', cmdline)
result = caplog.records[0].message
assert re.match(
r'Opening /foo/bar with \[.*python.*/foo/bar.*\]', result)
@pytest.mark.not_frozen
def test_cmdline_with_argument(self, caplog, config_stub):
config_stub.data = {'general': {'default-open-dispatcher': ''}}
executable = shlex.quote(sys.executable)
cmdline = '{} -c pass {{}} raboof'.format(executable)
utils.open_file('/foo/bar', cmdline)
result = caplog.records[0].message
assert re.match(
r"Opening /foo/bar with \[.*python.*/foo/bar.*'raboof'\]", result)
@pytest.mark.not_frozen
def test_setting_override(self, caplog, config_stub):
executable = shlex.quote(sys.executable)
cmdline = '{} -c pass'.format(executable)
config_stub.data = {'general': {'default-open-dispatcher': cmdline}}
utils.open_file('/foo/bar')
result = caplog.records[0].message
assert re.match(
r"Opening /foo/bar with \[.*python.*/foo/bar.*\]", result)
def test_system_default_application(self, caplog, config_stub, mocker):
config_stub.data = {'general': {'default-open-dispatcher': ''}}
m = mocker.patch('PyQt5.QtGui.QDesktopServices.openUrl', spec={},
new_callable=mocker.Mock)
utils.open_file('/foo/bar')
result = caplog.records[0].message
assert re.match(
r"Opening /foo/bar with the system application", result)
m.assert_called_with(QUrl('file:///foo/bar'))