diff --git a/misc/userscripts/password_fill b/misc/userscripts/password_fill index 5d709512c..327a55690 100755 --- a/misc/userscripts/password_fill +++ b/misc/userscripts/password_fill @@ -9,7 +9,7 @@ directly ask me via IRC (nickname thorsten\`) in #qutebrowser on freenode. $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset WARNING: the passwords are stored in qutebrowser's - debug log reachable via the url qute:log + debug log reachable via the url qute://log $blink!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!$reset Usage: run as a userscript form qutebrowser, e.g.: diff --git a/qutebrowser/browser/qtnetworkdownloads.py b/qutebrowser/browser/qtnetworkdownloads.py index 920673d4b..56149a8cc 100644 --- a/qutebrowser/browser/qtnetworkdownloads.py +++ b/qutebrowser/browser/qtnetworkdownloads.py @@ -273,7 +273,7 @@ class DownloadItem(downloads.AbstractDownloadItem): if self.fileobj is None or self._reply is None: # No filename has been set yet (so we don't empty the buffer) or we # got a readyRead after the reply was finished (which happens on - # qute:log for example). + # qute://log for example). return if not self._reply.isOpen(): raise OSError("Reply is closed!") diff --git a/qutebrowser/browser/qutescheme.py b/qutebrowser/browser/qutescheme.py index f7f074c54..5f25c24d1 100644 --- a/qutebrowser/browser/qutescheme.py +++ b/qutebrowser/browser/qutescheme.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""Backend-independent qute:* code. +"""Backend-independent qute://* code. Module attributes: pyeval_output: The output of the last :pyeval command. @@ -31,7 +31,7 @@ import time import urllib.parse import datetime -from PyQt5.QtCore import QUrlQuery +from PyQt5.QtCore import QUrlQuery, QUrl import qutebrowser from qutebrowser.config import config @@ -78,12 +78,25 @@ class QuteSchemeError(Exception): super().__init__(errorstring) -class add_handler: # pylint: disable=invalid-name +class Redirect(Exception): - """Decorator to register a qute:* URL handler. + """Exception to signal a redirect should happen. Attributes: - _name: The 'foo' part of qute:foo + url: The URL to redirect to, as a QUrl. + """ + + def __init__(self, url): + super().__init__(url.toDisplayString()) + self.url = url + + +class add_handler: # pylint: disable=invalid-name + + """Decorator to register a qute://* URL handler. + + Attributes: + _name: The 'foo' part of qute://foo backend: Limit which backends the handler can run with. """ @@ -106,7 +119,7 @@ class add_handler: # pylint: disable=invalid-name def wrong_backend_handler(self, url): """Show an error page about using the invalid backend.""" html = jinja.render('error.html', - title="Error while opening qute:url", + title="Error while opening qute://url", url=url.toDisplayString(), error='{} is not available with this ' 'backend'.format(url.toDisplayString()), @@ -128,13 +141,17 @@ def data_for_url(url): # A url like "qute:foo" is split as "scheme:path", not "scheme:host". log.misc.debug("url: {}, path: {}, host {}".format( url.toDisplayString(), path, host)) + if path and not host: + new_url = QUrl() + new_url.setScheme('qute') + new_url.setHost(path) + raise Redirect(new_url) + try: - handler = _HANDLERS[path] + handler = _HANDLERS[host] except KeyError: - try: - handler = _HANDLERS[host] - except KeyError: - raise NoHandlerFound(url) + raise NoHandlerFound(url) + try: mimetype, data = handler(url) except OSError as e: @@ -153,7 +170,7 @@ def data_for_url(url): @add_handler('bookmarks') def qute_bookmarks(_url): - """Handler for qute:bookmarks. Display all quickmarks / bookmarks.""" + """Handler for qute://bookmarks. Display all quickmarks / bookmarks.""" bookmarks = sorted(objreg.get('bookmark-manager').marks.items(), key=lambda x: x[1]) # Sort by title quickmarks = sorted(objreg.get('quickmark-manager').marks.items(), @@ -246,7 +263,7 @@ def history_data(start_time): # noqa @add_handler('history') def qute_history(url): - """Handler for qute:history. Display and serve history.""" + """Handler for qute://history. Display and serve history.""" if url.path() == '/data': # Use start_time in query or current time. try: @@ -309,7 +326,7 @@ def qute_history(url): @add_handler('javascript') def qute_javascript(url): - """Handler for qute:javascript. + """Handler for qute://javascript. Return content of file given as query parameter. """ @@ -323,7 +340,7 @@ def qute_javascript(url): @add_handler('pyeval') def qute_pyeval(_url): - """Handler for qute:pyeval.""" + """Handler for qute://pyeval.""" html = jinja.render('pre.html', title='pyeval', content=pyeval_output) return 'text/html', html @@ -331,7 +348,7 @@ def qute_pyeval(_url): @add_handler('version') @add_handler('verizon') def qute_version(_url): - """Handler for qute:version.""" + """Handler for qute://version.""" html = jinja.render('version.html', title='Version info', version=version.version(), copyright=qutebrowser.__copyright__) @@ -340,7 +357,7 @@ def qute_version(_url): @add_handler('plainlog') def qute_plainlog(url): - """Handler for qute:plainlog. + """Handler for qute://plainlog. An optional query parameter specifies the minimum log level to print. For example, qute://log?level=warning prints warnings and errors. @@ -360,7 +377,7 @@ def qute_plainlog(url): @add_handler('log') def qute_log(url): - """Handler for qute:log. + """Handler for qute://log. An optional query parameter specifies the minimum log level to print. For example, qute://log?level=warning prints warnings and errors. @@ -381,13 +398,13 @@ def qute_log(url): @add_handler('gpl') def qute_gpl(_url): - """Handler for qute:gpl. Return HTML content as string.""" + """Handler for qute://gpl. Return HTML content as string.""" return 'text/html', utils.read_file('html/COPYING.html') @add_handler('help') def qute_help(url): - """Handler for qute:help.""" + """Handler for qute://help.""" try: utils.read_file('html/doc/index.html') except OSError: diff --git a/qutebrowser/browser/webengine/webenginequtescheme.py b/qutebrowser/browser/webengine/webenginequtescheme.py index 6bc31f9be..cebf31356 100644 --- a/qutebrowser/browser/webengine/webenginequtescheme.py +++ b/qutebrowser/browser/webengine/webenginequtescheme.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""QtWebEngine specific qute:* handlers and glue code.""" +"""QtWebEngine specific qute://* handlers and glue code.""" from PyQt5.QtCore import QBuffer, QIODevice # pylint: disable=no-name-in-module,import-error,useless-suppression @@ -26,15 +26,15 @@ from PyQt5.QtWebEngineCore import (QWebEngineUrlSchemeHandler, # pylint: enable=no-name-in-module,import-error,useless-suppression from qutebrowser.browser import qutescheme -from qutebrowser.utils import log +from qutebrowser.utils import log, qtutils class QuteSchemeHandler(QWebEngineUrlSchemeHandler): - """Handle qute:* requests on QtWebEngine.""" + """Handle qute://* requests on QtWebEngine.""" def install(self, profile): - """Install the handler for qute: URLs on the given profile.""" + """Install the handler for qute:// URLs on the given profile.""" profile.installUrlSchemeHandler(b'qute', self) def requestStarted(self, job): @@ -58,12 +58,15 @@ class QuteSchemeHandler(QWebEngineUrlSchemeHandler): job.fail(QWebEngineUrlRequestJob.UrlNotFound) except qutescheme.QuteSchemeOSError: # FIXME:qtwebengine how do we show a better error here? - log.misc.exception("OSError while handling qute:* URL") + log.misc.exception("OSError while handling qute://* URL") job.fail(QWebEngineUrlRequestJob.UrlNotFound) except qutescheme.QuteSchemeError: # FIXME:qtwebengine how do we show a better error here? - log.misc.exception("Error while handling qute:* URL") + log.misc.exception("Error while handling qute://* URL") job.fail(QWebEngineUrlRequestJob.RequestFailed) + except qutescheme.Redirect as e: + qtutils.ensure_valid(e.url) + job.redirect(e.url) else: log.misc.debug("Returning {} data".format(mimetype)) diff --git a/qutebrowser/browser/webengine/webenginetab.py b/qutebrowser/browser/webengine/webenginetab.py index 76a526670..7eef0b03a 100644 --- a/qutebrowser/browser/webengine/webenginetab.py +++ b/qutebrowser/browser/webengine/webenginetab.py @@ -55,7 +55,7 @@ def init(): app = QApplication.instance() profile = QWebEngineProfile.defaultProfile() - log.init.debug("Initializing qute:* handler...") + log.init.debug("Initializing qute://* handler...") _qute_scheme_handler = webenginequtescheme.QuteSchemeHandler(parent=app) _qute_scheme_handler.install(profile) diff --git a/qutebrowser/browser/webkit/network/networkreply.py b/qutebrowser/browser/webkit/network/networkreply.py index 2cc5727be..9638daf4a 100644 --- a/qutebrowser/browser/webkit/network/networkreply.py +++ b/qutebrowser/browser/webkit/network/networkreply.py @@ -19,11 +19,15 @@ # # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . +# +# For some reason, a segfault will be triggered if the unnecessary lambdas in +# this file aren't there. +# pylint: disable=unnecessary-lambda """Special network replies..""" -from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest -from PyQt5.QtCore import pyqtSlot, QIODevice, QByteArray, QTimer +from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest, QNetworkAccessManager +from PyQt5.QtCore import pyqtSlot, QIODevice, QByteArray, QTimer, QUrl class FixedDataNetworkReply(QNetworkReply): @@ -114,9 +118,6 @@ class ErrorNetworkReply(QNetworkReply): # the device to avoid getting a warning. self.setOpenMode(QIODevice.ReadOnly) self.setError(error, errorstring) - # For some reason, a segfault will be triggered if these lambdas aren't - # there. - # pylint: disable=unnecessary-lambda QTimer.singleShot(0, lambda: self.error.emit(error)) QTimer.singleShot(0, lambda: self.finished.emit()) @@ -137,3 +138,16 @@ class ErrorNetworkReply(QNetworkReply): def isRunning(self): return False + + +class RedirectNetworkReply(QNetworkReply): + + """A reply which redirects to the given URL.""" + + def __init__(self, new_url, parent=None): + super().__init__(parent) + self.setAttribute(QNetworkRequest.RedirectionTargetAttribute, new_url) + QTimer.singleShot(0, lambda: self.finished.emit()) + + def readData(self, _maxlen): + return bytes() diff --git a/qutebrowser/browser/webkit/network/webkitqutescheme.py b/qutebrowser/browser/webkit/network/webkitqutescheme.py index 61ef760bc..34db29ee9 100644 --- a/qutebrowser/browser/webkit/network/webkitqutescheme.py +++ b/qutebrowser/browser/webkit/network/webkitqutescheme.py @@ -17,7 +17,7 @@ # You should have received a copy of the GNU General Public License # along with qutebrowser. If not, see . -"""QtWebKit specific qute:* handlers and glue code.""" +"""QtWebKit specific qute://* handlers and glue code.""" import mimetypes import functools @@ -28,13 +28,13 @@ from PyQt5.QtNetwork import QNetworkReply from qutebrowser.browser import pdfjs, qutescheme from qutebrowser.browser.webkit.network import schemehandler, networkreply -from qutebrowser.utils import jinja, log, message, objreg, usertypes +from qutebrowser.utils import jinja, log, message, objreg, usertypes, qtutils from qutebrowser.config import configexc, configdata class QuteSchemeHandler(schemehandler.SchemeHandler): - """Scheme handler for qute: URLs.""" + """Scheme handler for qute:// URLs.""" def createRequest(self, _op, request, _outgoing_data): """Create a new request. @@ -62,6 +62,9 @@ class QuteSchemeHandler(schemehandler.SchemeHandler): except qutescheme.QuteSchemeError as e: return networkreply.ErrorNetworkReply(request, e.errorstring, e.error, self.parent()) + except qutescheme.Redirect as e: + qtutils.ensure_valid(e.url) + return networkreply.RedirectNetworkReply(e.url, self.parent()) return networkreply.FixedDataNetworkReply(request, data, mimetype, self.parent()) @@ -69,15 +72,15 @@ class QuteSchemeHandler(schemehandler.SchemeHandler): class JSBridge(QObject): - """Javascript-bridge for special qute:... pages.""" + """Javascript-bridge for special qute://... pages.""" @pyqtSlot(str, str, str) def set(self, sectname, optname, value): - """Slot to set a setting from qute:settings.""" + """Slot to set a setting from qute://settings.""" # https://github.com/qutebrowser/qutebrowser/issues/727 if ((sectname, optname) == ('content', 'allow-javascript') and value == 'false'): - message.error("Refusing to disable javascript via qute:settings " + message.error("Refusing to disable javascript via qute://settings " "as it needs javascript support.") return try: @@ -88,7 +91,7 @@ class JSBridge(QObject): @qutescheme.add_handler('settings', backend=usertypes.Backend.QtWebKit) def qute_settings(_url): - """Handler for qute:settings. View/change qute configuration.""" + """Handler for qute://settings. View/change qute configuration.""" config_getter = functools.partial(objreg.get('config').get, raw=True) html = jinja.render('settings.html', title='settings', config=configdata, confget=config_getter) diff --git a/qutebrowser/browser/webkit/webview.py b/qutebrowser/browser/webkit/webview.py index 12670be4f..0c3aa179e 100644 --- a/qutebrowser/browser/webkit/webview.py +++ b/qutebrowser/browser/webkit/webview.py @@ -140,7 +140,7 @@ class WebView(QWebView): @pyqtSlot() def add_js_bridge(self): - """Add the javascript bridge for qute:... pages.""" + """Add the javascript bridge for qute://... pages.""" frame = self.sender() if not isinstance(frame, QWebFrame): log.webview.error("Got non-QWebFrame {!r} in " diff --git a/qutebrowser/config/config.py b/qutebrowser/config/config.py index 1e0cec92b..82c6aca00 100644 --- a/qutebrowser/config/config.py +++ b/qutebrowser/config/config.py @@ -805,7 +805,7 @@ class ConfigManager(QObject): if section_ is None and option is None: tabbed_browser = objreg.get('tabbed-browser', scope='window', window=win_id) - tabbed_browser.openurl(QUrl('qute:settings'), newtab=False) + tabbed_browser.openurl(QUrl('qute://settings'), newtab=False) return if option.endswith('?') and option != '?': diff --git a/qutebrowser/config/configdata.py b/qutebrowser/config/configdata.py index 1fbc22937..2c5b27808 100644 --- a/qutebrowser/config/configdata.py +++ b/qutebrowser/config/configdata.py @@ -1689,7 +1689,7 @@ KEY_DATA = collections.OrderedDict([ ('home', ['']), ('stop', ['']), ('print', ['']), - ('open qute:settings', ['Ss']), + ('open qute://settings', ['Ss']), ('follow-selected', RETURN_KEYS), ('follow-selected -t', ['', '']), ('repeat-command', ['.']), diff --git a/qutebrowser/misc/utilcmds.py b/qutebrowser/misc/utilcmds.py index fc777c2e9..de43c71f4 100644 --- a/qutebrowser/misc/utilcmds.py +++ b/qutebrowser/misc/utilcmds.py @@ -228,7 +228,7 @@ def debug_pyeval(s, quiet=False): else: tabbed_browser = objreg.get('tabbed-browser', scope='window', window='last-focused') - tabbed_browser.openurl(QUrl('qute:pyeval'), newtab=True) + tabbed_browser.openurl(QUrl('qute://pyeval'), newtab=True) @cmdutils.register(debug=True) diff --git a/scripts/dev/run_vulture.py b/scripts/dev/run_vulture.py index 55f4d9c86..f1eed1803 100755 --- a/scripts/dev/run_vulture.py +++ b/scripts/dev/run_vulture.py @@ -74,7 +74,7 @@ def whitelist_generator(): yield 'PyQt5.QtWebKit.QWebPage.ErrorPageExtensionReturn().fileNames' yield 'PyQt5.QtWidgets.QStyleOptionViewItem.backgroundColor' - ## qute:... handlers + ## qute://... handlers for name in qutescheme._HANDLERS: # pylint: disable=protected-access yield 'qutebrowser.browser.qutescheme.qute_' + name diff --git a/tests/end2end/features/history.feature b/tests/end2end/features/history.feature index 27454c522..613b660af 100644 --- a/tests/end2end/features/history.feature +++ b/tests/end2end/features/history.feature @@ -83,6 +83,14 @@ Feature: Page history Then the page should contain the plaintext "3.txt" Then the page should contain the plaintext "4.txt" + Scenario: Listing history with qute:history redirect + When I open data/numbers/3.txt + And I open data/numbers/4.txt + And I open qute:history without waiting + And I wait until qute://history is loaded + Then the page should contain the plaintext "3.txt" + Then the page should contain the plaintext "4.txt" + ## Bugs @qtwebengine_skip @qtwebkit_ng_skip diff --git a/tests/end2end/features/misc.feature b/tests/end2end/features/misc.feature index 0aeb65f92..e3bb13b6f 100644 --- a/tests/end2end/features/misc.feature +++ b/tests/end2end/features/misc.feature @@ -405,12 +405,12 @@ Feature: Various utility commands. # :pyeval Scenario: Running :pyeval When I run :debug-pyeval 1+1 - And I wait until qute:pyeval is loaded + And I wait until qute://pyeval is loaded Then the page should contain the plaintext "2" Scenario: Causing exception in :pyeval When I run :debug-pyeval 1/0 - And I wait until qute:pyeval is loaded + And I wait until qute://pyeval is loaded Then the page should contain the plaintext "ZeroDivisionError" Scenario: Running :pyeval with --quiet @@ -512,12 +512,12 @@ Feature: Various utility commands. When I run :messages cataclysmic Then the error "Invalid log level cataclysmic!" should be shown - Scenario: Using qute:log directly - When I open qute:log + Scenario: Using qute://log directly + When I open qute://log Then no crash should happen - Scenario: Using qute:plainlog directly - When I open qute:plainlog + Scenario: Using qute://plainlog directly + When I open qute://plainlog Then no crash should happen Scenario: Using :messages without messages diff --git a/tests/end2end/features/set.feature b/tests/end2end/features/set.feature index b2b165542..bc144eb6e 100644 --- a/tests/end2end/features/set.feature +++ b/tests/end2end/features/set.feature @@ -78,15 +78,15 @@ Feature: Setting settings. When I run :set -t colors statusbar.bg green Then colors -> statusbar.bg should be green - # qute:settings isn't actually implemented on QtWebEngine, but this works + # qute://settings isn't actually implemented on QtWebEngine, but this works # (and displays a page saying it's not available) - Scenario: Opening qute:settings + Scenario: Opening qute://settings When I run :set - And I wait until qute:settings is loaded + And I wait until qute://settings is loaded Then the following tabs should be open: - - qute:settings (active) + - qute://settings (active) - @qtwebengine_todo: qute:settings is not implemented yet + @qtwebengine_todo: qute://settings is not implemented yet Scenario: Focusing input fields in qute://settings and entering valid value When I set general -> ignore-case to false And I open qute://settings @@ -101,7 +101,7 @@ Feature: Setting settings. And I press the key "" Then general -> ignore-case should be true - @qtwebengine_todo: qute:settings is not implemented yet + @qtwebengine_todo: qute://settings is not implemented yet Scenario: Focusing input fields in qute://settings and entering invalid value When I open qute://settings # scroll to the right - the table does not fit in the default screen diff --git a/tests/end2end/features/urlmarks.feature b/tests/end2end/features/urlmarks.feature index 873d83563..f233215b8 100644 --- a/tests/end2end/features/urlmarks.feature +++ b/tests/end2end/features/urlmarks.feature @@ -225,12 +225,12 @@ Feature: quickmarks and bookmarks Scenario: Listing quickmarks When I run :quickmark-add http://localhost:(port)/data/numbers/20.txt twenty And I run :quickmark-add http://localhost:(port)/data/numbers/21.txt twentyone - And I open qute:bookmarks + And I open qute://bookmarks Then the page should contain the plaintext "twenty" And the page should contain the plaintext "twentyone" Scenario: Listing bookmarks When I open data/title.html in a new tab And I run :bookmark-add - And I open qute:bookmarks + And I open qute://bookmarks Then the page should contain the plaintext "Test title" diff --git a/tests/end2end/features/utilcmds.feature b/tests/end2end/features/utilcmds.feature index 5696de75d..7fd5952a8 100644 --- a/tests/end2end/features/utilcmds.feature +++ b/tests/end2end/features/utilcmds.feature @@ -145,7 +145,7 @@ Feature: Miscellaneous utility commands exposed to the user. And I run :message-info oldstuff And I run :repeat 20 message-info otherstuff And I run :message-info newstuff - And I open qute:log + And I open qute://log Then the page should contain the plaintext "newstuff" And the page should not contain the plaintext "oldstuff" diff --git a/tests/end2end/test_invocations.py b/tests/end2end/test_invocations.py index d6697ec29..ed0222014 100644 --- a/tests/end2end/test_invocations.py +++ b/tests/end2end/test_invocations.py @@ -138,10 +138,10 @@ def test_misconfigured_user_dirs(request, httpbin, temp_basedir_env, def test_no_loglines(request, quteproc_new): - """Test qute:log with --loglines=0.""" + """Test qute://log with --loglines=0.""" quteproc_new.start(args=['--temp-basedir', '--loglines=0'] + _base_args(request.config)) - quteproc_new.open_path('qute:log') + quteproc_new.open_path('qute://log') assert quteproc_new.get_content() == 'Log output was disabled.' diff --git a/tests/unit/browser/webkit/network/test_networkreply.py b/tests/unit/browser/webkit/network/test_networkreply.py index 7a9c89393..7505dbc8c 100644 --- a/tests/unit/browser/webkit/network/test_networkreply.py +++ b/tests/unit/browser/webkit/network/test_networkreply.py @@ -91,3 +91,10 @@ def test_error_network_reply(qtbot, req): assert reply.readData(1) == b'' assert reply.error() == QNetworkReply.UnknownNetworkError assert reply.errorString() == "This is an error" + + +def test_redirect_network_reply(): + url = QUrl('https://www.example.com/') + reply = networkreply.RedirectNetworkReply(url) + assert reply.readData(1) == b'' + assert reply.attribute(QNetworkRequest.RedirectionTargetAttribute) == url diff --git a/tests/unit/utils/test_urlutils.py b/tests/unit/utils/test_urlutils.py index f944f95b2..4729bd661 100644 --- a/tests/unit/utils/test_urlutils.py +++ b/tests/unit/utils/test_urlutils.py @@ -265,6 +265,7 @@ class TestFuzzyUrl: ('file:///tmp/foo', True), ('about:blank', True), ('qute:version', True), + ('qute://version', True), ('http://www.qutebrowser.org/', False), ('www.qutebrowser.org', False), ]) @@ -317,9 +318,11 @@ def test_get_search_url_invalid(urlutils_config_stub, url): (True, True, False, 'file:///tmp/foo'), (True, True, False, 'about:blank'), (True, True, False, 'qute:version'), + (True, True, False, 'qute://version'), (True, True, False, 'localhost'), # _has_explicit_scheme False, special_url True (True, True, False, 'qute::foo'), + (True, True, False, 'qute:://foo'), # Invalid URLs (False, False, False, ''), (False, True, False, 'onlyscheme:'),