qutebrowser/qutebrowser/browser/qutescheme.py

424 lines
13 KiB
Python
Raw Normal View History

2016-09-14 10:18:25 +02:00
# vim: ft=python fileencoding=utf-8 sts=4 sw=4 et:
2017-05-09 21:37:03 +02:00
# Copyright 2016-2017 Florian Bruhin (The Compiler) <mail@qutebrowser.org>
2016-09-14 10:18:25 +02:00
#
# 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/>.
"""Backend-independent qute://* code.
2016-09-14 10:18:25 +02:00
Module attributes:
pyeval_output: The output of the last :pyeval command.
_HANDLERS: The handlers registered via decorators.
"""
2017-02-26 13:07:30 +01:00
import json
2017-03-04 15:37:48 +01:00
import os
import time
2016-09-14 10:18:25 +02:00
import urllib.parse
2017-04-06 20:36:54 +02:00
import datetime
import pkg_resources
2016-09-14 10:18:25 +02:00
from PyQt5.QtCore import QUrlQuery, QUrl
2016-09-14 10:18:25 +02:00
import qutebrowser
from qutebrowser.config import config, configdata, configexc
2016-09-14 10:18:25 +02:00
from qutebrowser.utils import (version, utils, jinja, log, message, docutils,
2017-04-04 16:20:55 +02:00
objreg, usertypes, qtutils)
from qutebrowser.misc import objects
2016-09-14 10:18:25 +02:00
pyeval_output = ":pyeval was never called"
_HANDLERS = {}
class NoHandlerFound(Exception):
"""Raised when no handler was found for the given URL."""
pass
class QuteSchemeOSError(Exception):
"""Called when there was an OSError inside a handler."""
pass
2016-09-14 11:04:37 +02:00
class QuteSchemeError(Exception):
"""Exception to signal that a handler should return an ErrorReply.
Attributes correspond to the arguments in
networkreply.ErrorNetworkReply.
Attributes:
errorstring: Error string to print.
error: Numerical error value.
"""
def __init__(self, errorstring, error):
self.errorstring = errorstring
self.error = error
super().__init__(errorstring)
class Redirect(Exception):
"""Exception to signal a redirect should happen.
Attributes:
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.
"""
def __init__(self, name, backend=None):
self._name = name
self._backend = backend
self._function = None
def __call__(self, function):
self._function = function
_HANDLERS[self._name] = self.wrapper
2016-09-14 10:18:25 +02:00
return function
def wrapper(self, *args, **kwargs):
if self._backend is not None and objects.backend != self._backend:
return self.wrong_backend_handler(*args, **kwargs)
else:
return self._function(*args, **kwargs)
def wrong_backend_handler(self, url):
2016-09-14 12:00:29 +02:00
"""Show an error page about using the invalid backend."""
html = jinja.render('error.html',
title="Error while opening qute://url",
url=url.toDisplayString(),
error='{} is not available with this '
2017-06-15 14:31:21 +02:00
'backend'.format(url.toDisplayString()))
return 'text/html', html
2016-09-14 10:18:25 +02:00
def data_for_url(url):
"""Get the data to show for the given URL.
Args:
url: The QUrl to show.
Return:
A (mimetype, data) tuple.
"""
path = url.path()
host = url.host()
# 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)
new_url.setPath('/')
if new_url.host(): # path was a valid host
raise Redirect(new_url)
2016-09-14 10:18:25 +02:00
try:
handler = _HANDLERS[host]
2016-09-14 10:18:25 +02:00
except KeyError:
raise NoHandlerFound(url)
2016-09-14 10:18:25 +02:00
try:
mimetype, data = handler(url)
2016-09-14 10:18:25 +02:00
except OSError as e:
# FIXME:qtwebengine how to handle this?
raise QuteSchemeOSError(e)
except QuteSchemeError as e:
raise
assert mimetype is not None, url
if mimetype == 'text/html' and isinstance(data, str):
# We let handlers return HTML as text
data = data.encode('utf-8', errors='xmlcharrefreplace')
2016-09-14 10:18:25 +02:00
return mimetype, data
@add_handler('bookmarks')
def qute_bookmarks(_url):
"""Handler for qute://bookmarks. Display all quickmarks / bookmarks."""
2016-09-14 10:18:25 +02:00
bookmarks = sorted(objreg.get('bookmark-manager').marks.items(),
key=lambda x: x[1]) # Sort by title
quickmarks = sorted(objreg.get('quickmark-manager').marks.items(),
key=lambda x: x[0]) # Sort by name
html = jinja.render('bookmarks.html',
title='Bookmarks',
bookmarks=bookmarks,
quickmarks=quickmarks)
return 'text/html', html
2016-09-14 10:18:25 +02:00
def history_data(start_time, offset=None):
"""Return history data.
2017-03-28 15:34:47 +02:00
Arguments:
start_time: select history starting from this timestamp.
offset: number of items to skip
2017-03-28 15:34:47 +02:00
"""
# history atimes are stored as ints, ensure start_time is not a float
start_time = int(start_time)
hist = objreg.get('web-history')
if offset is not None:
entries = hist.entries_before(start_time, limit=1000, offset=offset)
else:
2017-03-28 15:34:47 +02:00
# end is 24hrs earlier than start
end_time = start_time - 24*60*60
entries = hist.entries_between(end_time, start_time)
2017-03-28 15:34:47 +02:00
return [{"url": e.url, "title": e.title or e.url, "time": e.atime}
for e in entries]
2017-03-28 15:34:47 +02:00
@add_handler('history')
def qute_history(url):
"""Handler for qute://history. Display and serve history."""
2017-02-26 16:13:05 +01:00
if url.path() == '/data':
try:
offset = QUrlQuery(url).queryItemValue("offset")
offset = int(offset) if offset else None
except ValueError as e:
raise QuteSchemeError("Query parameter offset is invalid", e)
2017-02-26 13:07:30 +01:00
# Use start_time in query or current time.
try:
start_time = QUrlQuery(url).queryItemValue("start_time")
start_time = float(start_time) if start_time else time.time()
except ValueError as e:
raise QuteSchemeError("Query parameter start_time is invalid", e)
2017-02-06 20:04:32 +01:00
return 'text/html', json.dumps(history_data(start_time, offset))
else:
2017-03-28 15:34:47 +02:00
if (
2017-06-14 19:33:47 +02:00
config.val.content.javascript.enabled and
(objects.backend == usertypes.Backend.QtWebEngine or
qtutils.is_qtwebkit_ng())
2017-03-28 15:34:47 +02:00
):
return 'text/html', jinja.render(
'history.html',
title='History',
gap_interval=config.val.history_gap_interval
2017-03-28 15:34:47 +02:00
)
else:
# Get current date from query parameter, if not given choose today.
2017-04-06 21:37:23 +02:00
curr_date = datetime.date.today()
2017-03-28 15:34:47 +02:00
try:
query_date = QUrlQuery(url).queryItemValue("date")
if query_date:
2017-04-06 20:36:54 +02:00
curr_date = datetime.datetime.strptime(query_date,
2017-03-28 15:34:47 +02:00
"%Y-%m-%d").date()
except ValueError:
log.misc.debug("Invalid date passed to qute:history: " +
query_date)
2017-04-06 20:36:54 +02:00
one_day = datetime.timedelta(days=1)
2017-03-28 15:34:47 +02:00
next_date = curr_date + one_day
prev_date = curr_date - one_day
# start_time is the last second of curr_date
start_time = time.mktime(next_date.timetuple()) - 1
history = [
2017-04-06 20:36:54 +02:00
(i["url"], i["title"],
datetime.datetime.fromtimestamp(i["time"]),
QUrl(i["url"]).host())
for i in history_data(start_time)
2017-03-28 15:34:47 +02:00
]
return 'text/html', jinja.render(
'history_nojs.html',
title='History',
history=history,
curr_date=curr_date,
next_date=next_date,
prev_date=prev_date,
2017-04-06 21:37:23 +02:00
today=datetime.date.today(),
2017-03-28 15:34:47 +02:00
)
2017-02-06 20:04:32 +01:00
2017-02-27 18:37:24 +01:00
@add_handler('javascript')
def qute_javascript(url):
"""Handler for qute://javascript.
2017-02-27 18:37:24 +01:00
Return content of file given as query parameter.
"""
path = url.path()
if path:
2017-03-04 15:37:48 +01:00
path = "javascript" + os.sep.join(path.split('/'))
return 'text/html', utils.read_file(path, binary=False)
2017-02-27 18:37:24 +01:00
else:
raise QuteSchemeError("No file specified", ValueError())
2016-09-14 10:18:25 +02:00
@add_handler('pyeval')
def qute_pyeval(_url):
"""Handler for qute://pyeval."""
2016-09-14 10:18:25 +02:00
html = jinja.render('pre.html', title='pyeval', content=pyeval_output)
return 'text/html', html
2016-09-14 10:18:25 +02:00
@add_handler('version')
@add_handler('verizon')
def qute_version(_url):
"""Handler for qute://version."""
2016-09-14 10:18:25 +02:00
html = jinja.render('version.html', title='Version info',
version=version.version(),
copyright=qutebrowser.__copyright__)
return 'text/html', html
2016-09-14 10:18:25 +02:00
@add_handler('plainlog')
def qute_plainlog(url):
"""Handler for qute://plainlog.
2016-09-14 10:18:25 +02:00
An optional query parameter specifies the minimum log level to print.
For example, qute://log?level=warning prints warnings and errors.
Level can be one of: vdebug, debug, info, warning, error, critical.
"""
if log.ram_handler is None:
text = "Log output was disabled."
else:
try:
level = urllib.parse.parse_qs(url.query())['level'][0]
except KeyError:
level = 'vdebug'
text = log.ram_handler.dump_log(html=False, level=level)
html = jinja.render('pre.html', title='log', content=text)
return 'text/html', html
2016-09-14 10:18:25 +02:00
@add_handler('log')
def qute_log(url):
"""Handler for qute://log.
2016-09-14 10:18:25 +02:00
An optional query parameter specifies the minimum log level to print.
For example, qute://log?level=warning prints warnings and errors.
Level can be one of: vdebug, debug, info, warning, error, critical.
"""
if log.ram_handler is None:
html_log = None
else:
try:
level = urllib.parse.parse_qs(url.query())['level'][0]
except KeyError:
level = 'vdebug'
html_log = log.ram_handler.dump_log(html=True, level=level)
html = jinja.render('log.html', title='log', content=html_log)
return 'text/html', html
2016-09-14 10:18:25 +02:00
@add_handler('gpl')
def qute_gpl(_url):
"""Handler for qute://gpl. Return HTML content as string."""
return 'text/html', utils.read_file('html/COPYING.html')
2016-09-14 10:18:25 +02:00
@add_handler('help')
def qute_help(url):
"""Handler for qute://help."""
2016-09-14 10:18:25 +02:00
try:
utils.read_file('html/doc/index.html')
except OSError:
html = jinja.render(
'error.html',
title="Error while loading documentation",
url=url.toDisplayString(),
error="This most likely means the documentation was not generated "
"properly. If you are running qutebrowser from the git "
"repository, please run scripts/asciidoc2html.py. "
"If you're running a released version this is a bug, please "
2017-06-15 14:31:21 +02:00
"use :report to report it.")
return 'text/html', html
2016-09-14 10:18:25 +02:00
urlpath = url.path()
if not urlpath or urlpath == '/':
urlpath = 'index.html'
else:
urlpath = urlpath.lstrip('/')
if not docutils.docs_up_to_date(urlpath):
2016-09-14 20:52:32 +02:00
message.error("Your documentation is outdated! Please re-run "
"scripts/asciidoc2html.py.")
2016-09-14 10:18:25 +02:00
path = 'html/doc/{}'.format(urlpath)
if urlpath.endswith('.png'):
return 'image/png', utils.read_file(path, binary=True)
2016-09-14 10:18:25 +02:00
else:
data = utils.read_file(path)
return 'text/html', data
@add_handler('backend-warning')
2017-05-30 17:07:31 +02:00
def qute_backend_warning(_url):
"""Handler for qute://backend-warning."""
html = jinja.render('backend-warning.html',
distribution=version.distribution(),
Distribution=version.Distribution,
version=pkg_resources.parse_version,
title="Legacy backend warning")
return 'text/html', html
def _qute_settings_set(url):
"""Handler for qute://settings/set."""
query = QUrlQuery(url)
option = query.queryItemValue('option', QUrl.FullyDecoded)
value = query.queryItemValue('value', QUrl.FullyDecoded)
# https://github.com/qutebrowser/qutebrowser/issues/727
if option == 'content.javascript.enabled' and value == 'false':
msg = ("Refusing to disable javascript via qute://settings "
"as it needs javascript support.")
message.error(msg)
return 'text/html', b'error: ' + msg.encode('utf-8')
try:
config.instance.set_str(option, value, save_yaml=True)
return 'text/html', b'ok'
except configexc.Error as e:
message.error(str(e))
return 'text/html', b'error: ' + str(e).encode('utf-8')
@add_handler('settings')
def qute_settings(url):
"""Handler for qute://settings. View/change qute configuration."""
if url.path() == '/set':
return _qute_settings_set(url)
html = jinja.render('settings.html', title='settings',
2017-06-15 19:05:55 +02:00
configdata=configdata,
confget=config.instance.get_str)
return 'text/html', html