2020-10-13 09:40:51 -04:00
|
|
|
from future.utils import PY2
|
|
|
|
from past.builtins import unicode
|
2008-03-05 14:59:56 -07:00
|
|
|
|
2016-01-04 16:00:59 +00:00
|
|
|
import time
|
2017-01-19 15:39:53 -07:00
|
|
|
import json
|
2020-09-18 15:01:53 -04:00
|
|
|
from functools import wraps
|
2016-01-04 16:00:59 +00:00
|
|
|
|
2020-10-15 11:43:42 -04:00
|
|
|
from hyperlink import (
|
|
|
|
DecodedURL,
|
|
|
|
)
|
|
|
|
|
2020-10-16 09:30:09 -04:00
|
|
|
from eliot import (
|
|
|
|
Message,
|
|
|
|
start_action,
|
|
|
|
)
|
|
|
|
from eliot.twisted import (
|
|
|
|
DeferredContext,
|
|
|
|
)
|
|
|
|
|
2020-09-22 21:14:00 -04:00
|
|
|
from twisted.web import (
|
|
|
|
http,
|
|
|
|
resource,
|
|
|
|
template,
|
|
|
|
)
|
2020-10-15 11:43:42 -04:00
|
|
|
from twisted.web.template import (
|
|
|
|
tags,
|
|
|
|
)
|
2020-10-15 15:15:54 -04:00
|
|
|
from twisted.web.server import (
|
|
|
|
NOT_DONE_YET,
|
|
|
|
UnsupportedMethod,
|
|
|
|
)
|
2020-10-15 11:43:42 -04:00
|
|
|
from twisted.web.util import (
|
2020-10-15 16:14:06 -04:00
|
|
|
DeferredResource,
|
2020-10-15 11:43:42 -04:00
|
|
|
FailureElement,
|
|
|
|
redirectTo,
|
|
|
|
)
|
|
|
|
from twisted.python.reflect import (
|
|
|
|
fullyQualifiedName,
|
|
|
|
)
|
2009-06-24 19:17:07 -07:00
|
|
|
from twisted.python import log
|
2020-10-15 11:43:42 -04:00
|
|
|
from twisted.python.failure import (
|
|
|
|
Failure,
|
|
|
|
)
|
|
|
|
from twisted.internet.defer import (
|
|
|
|
maybeDeferred,
|
|
|
|
)
|
|
|
|
from twisted.web.resource import (
|
|
|
|
IResource,
|
|
|
|
)
|
2020-10-13 09:40:51 -04:00
|
|
|
from twisted.web.iweb import IRequest as ITwistedRequest
|
|
|
|
if PY2:
|
|
|
|
from nevow.appserver import DefaultExceptionHandler
|
|
|
|
from nevow.inevow import IRequest as INevowRequest
|
|
|
|
else:
|
|
|
|
class DefaultExceptionHandler:
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
raise NotImplementedError("Still not ported to Python 3")
|
|
|
|
INevowRequest = None
|
|
|
|
|
2011-08-24 08:59:28 -07:00
|
|
|
from allmydata import blacklist
|
2020-09-22 21:14:00 -04:00
|
|
|
from allmydata.interfaces import (
|
|
|
|
EmptyPathnameComponentError,
|
|
|
|
ExistingChildError,
|
|
|
|
FileTooLargeError,
|
|
|
|
MustBeDeepImmutableError,
|
|
|
|
MustBeReadonlyError,
|
|
|
|
MustNotBeUnknownRWError,
|
|
|
|
NoSharesError,
|
|
|
|
NoSuchChildError,
|
|
|
|
NotEnoughSharesError,
|
|
|
|
MDMF_VERSION,
|
|
|
|
SDMF_VERSION,
|
|
|
|
)
|
2009-03-07 04:54:08 -07:00
|
|
|
from allmydata.mutable.common import UnrecoverableFileError
|
2020-09-22 21:14:00 -04:00
|
|
|
from allmydata.util.time_format import (
|
|
|
|
format_delta,
|
|
|
|
format_time,
|
|
|
|
)
|
|
|
|
from allmydata.util.encodingutil import (
|
|
|
|
quote_output,
|
|
|
|
to_bytes,
|
|
|
|
)
|
2011-01-21 17:40:06 -08:00
|
|
|
|
2020-08-20 14:17:06 -04:00
|
|
|
# Originally part of this module, so still part of its API:
|
|
|
|
from .common_py3 import ( # noqa: F401
|
2020-09-18 14:10:09 -04:00
|
|
|
get_arg, abbreviate_time, MultiFormatResource, WebError,
|
2020-08-20 14:17:06 -04:00
|
|
|
)
|
|
|
|
|
2008-03-05 14:59:56 -07:00
|
|
|
|
2014-10-07 13:11:31 -06:00
|
|
|
def get_filenode_metadata(filenode):
|
2014-10-09 20:37:32 -06:00
|
|
|
metadata = {'mutable': filenode.is_mutable()}
|
2014-10-07 13:11:31 -06:00
|
|
|
if metadata['mutable']:
|
|
|
|
mutable_type = filenode.get_version()
|
|
|
|
assert mutable_type in (SDMF_VERSION, MDMF_VERSION)
|
|
|
|
if mutable_type == MDMF_VERSION:
|
|
|
|
file_format = "MDMF"
|
|
|
|
else:
|
|
|
|
file_format = "SDMF"
|
|
|
|
else:
|
|
|
|
file_format = "CHK"
|
|
|
|
metadata['format'] = file_format
|
2014-10-10 20:36:08 +01:00
|
|
|
size = filenode.get_size()
|
|
|
|
if size is not None:
|
|
|
|
metadata['size'] = size
|
2014-10-07 13:11:31 -06:00
|
|
|
return metadata
|
2013-05-19 23:27:23 +01:00
|
|
|
|
2008-03-05 15:12:42 -07:00
|
|
|
def boolean_of_arg(arg):
|
2008-05-19 12:57:04 -07:00
|
|
|
# TODO: ""
|
2013-04-05 06:36:14 +01:00
|
|
|
if arg.lower() not in ("true", "t", "1", "false", "f", "0", "on", "off"):
|
|
|
|
raise WebError("invalid boolean argument: %r" % (arg,), http.BAD_REQUEST)
|
2008-03-05 15:12:42 -07:00
|
|
|
return arg.lower() in ("true", "t", "1", "on")
|
|
|
|
|
2009-07-19 20:47:23 -07:00
|
|
|
def parse_replace_arg(replace):
|
2009-07-20 11:38:03 -04:00
|
|
|
if replace.lower() == "only-files":
|
2009-07-19 20:47:23 -07:00
|
|
|
return replace
|
2013-04-05 06:36:14 +01:00
|
|
|
try:
|
2009-07-19 20:47:23 -07:00
|
|
|
return boolean_of_arg(replace)
|
2013-04-05 06:36:14 +01:00
|
|
|
except WebError:
|
|
|
|
raise WebError("invalid replace= argument: %r" % (replace,), http.BAD_REQUEST)
|
2009-07-19 20:47:23 -07:00
|
|
|
|
2011-08-06 17:43:48 -07:00
|
|
|
|
2011-10-13 09:29:51 -07:00
|
|
|
def get_format(req, default="CHK"):
|
|
|
|
arg = get_arg(req, "format", None)
|
2011-08-06 17:43:48 -07:00
|
|
|
if not arg:
|
2011-10-13 09:29:51 -07:00
|
|
|
if boolean_of_arg(get_arg(req, "mutable", "false")):
|
|
|
|
return "SDMF"
|
|
|
|
return default
|
|
|
|
if arg.upper() == "CHK":
|
|
|
|
return "CHK"
|
|
|
|
elif arg.upper() == "SDMF":
|
|
|
|
return "SDMF"
|
|
|
|
elif arg.upper() == "MDMF":
|
|
|
|
return "MDMF"
|
|
|
|
else:
|
|
|
|
raise WebError("Unknown format: %s, I know CHK, SDMF, MDMF" % arg,
|
|
|
|
http.BAD_REQUEST)
|
2011-08-06 17:43:48 -07:00
|
|
|
|
2011-10-13 09:29:51 -07:00
|
|
|
def get_mutable_type(file_format): # accepts result of get_format()
|
|
|
|
if file_format == "SDMF":
|
2011-08-06 17:43:48 -07:00
|
|
|
return SDMF_VERSION
|
2011-10-13 09:29:51 -07:00
|
|
|
elif file_format == "MDMF":
|
|
|
|
return MDMF_VERSION
|
|
|
|
else:
|
2011-10-13 09:32:29 -07:00
|
|
|
# this is also used to identify which formats are mutable. Use
|
|
|
|
# if get_mutable_type(file_format) is not None:
|
|
|
|
# do_mutable()
|
|
|
|
# else:
|
|
|
|
# do_immutable()
|
2011-10-13 09:29:51 -07:00
|
|
|
return None
|
2011-08-06 17:43:48 -07:00
|
|
|
|
|
|
|
|
|
|
|
def parse_offset_arg(offset):
|
|
|
|
# XXX: This will raise a ValueError when invoked on something that
|
|
|
|
# is not an integer. Is that okay? Or do we want a better error
|
|
|
|
# message? Since this call is going to be used by programmers and
|
|
|
|
# their tools rather than users (through the wui), it is not
|
|
|
|
# inconsistent to return that, I guess.
|
|
|
|
if offset is not None:
|
|
|
|
offset = int(offset)
|
|
|
|
|
|
|
|
return offset
|
|
|
|
|
|
|
|
|
2008-10-21 17:03:07 -07:00
|
|
|
def get_root(ctx_or_req):
|
2020-10-13 09:40:51 -04:00
|
|
|
if PY2:
|
|
|
|
req = INevowRequest(ctx_or_req)
|
|
|
|
else:
|
|
|
|
req = ITwistedRequest(ctx_or_req)
|
2019-09-24 13:55:40 -06:00
|
|
|
depth = len(req.prepath) + len(req.postpath)
|
2008-10-21 17:03:07 -07:00
|
|
|
link = "/".join([".."] * depth)
|
|
|
|
return link
|
|
|
|
|
2008-03-06 21:16:38 -07:00
|
|
|
|
2009-10-25 18:13:21 -07:00
|
|
|
def convert_children_json(nodemaker, children_json):
|
2009-10-17 12:28:29 -07:00
|
|
|
"""I convert the JSON output of GET?t=json into the dict-of-nodes input
|
|
|
|
to both dirnode.create_subdirectory() and
|
2009-11-17 23:09:00 -08:00
|
|
|
client.create_directory(initial_children=). This is used by
|
|
|
|
t=mkdir-with-children and t=mkdir-immutable"""
|
|
|
|
children = {}
|
2009-10-25 18:13:21 -07:00
|
|
|
if children_json:
|
2017-01-19 15:39:53 -07:00
|
|
|
data = json.loads(children_json)
|
2010-07-11 12:55:25 -07:00
|
|
|
for (namex, (ctype, propdict)) in data.iteritems():
|
|
|
|
namex = unicode(namex)
|
2020-08-14 13:49:48 -04:00
|
|
|
writecap = to_bytes(propdict.get("rw_uri"))
|
|
|
|
readcap = to_bytes(propdict.get("ro_uri"))
|
2009-10-12 19:34:44 -07:00
|
|
|
metadata = propdict.get("metadata", {})
|
2010-07-11 12:55:25 -07:00
|
|
|
# name= argument is just for error reporting
|
|
|
|
childnode = nodemaker.create_from_cap(writecap, readcap, name=namex)
|
|
|
|
children[namex] = (childnode, metadata)
|
2009-11-17 23:09:00 -08:00
|
|
|
return children
|
2009-10-12 19:34:44 -07:00
|
|
|
|
2008-03-06 21:16:38 -07:00
|
|
|
|
2010-08-15 07:19:33 -07:00
|
|
|
def compute_rate(bytes, seconds):
|
|
|
|
if bytes is None:
|
|
|
|
return None
|
|
|
|
|
|
|
|
if seconds is None or seconds == 0:
|
|
|
|
return None
|
|
|
|
|
|
|
|
# negative values don't make sense here
|
|
|
|
assert bytes > -1
|
|
|
|
assert seconds > 0
|
|
|
|
|
|
|
|
return 1.0 * bytes / seconds
|
|
|
|
|
2008-03-06 21:16:38 -07:00
|
|
|
def abbreviate_rate(data):
|
|
|
|
# 21.8kBps, 554.4kBps 4.37MBps
|
|
|
|
if data is None:
|
|
|
|
return ""
|
|
|
|
r = float(data)
|
|
|
|
if r > 1000000:
|
|
|
|
return "%1.2fMBps" % (r/1000000)
|
|
|
|
if r > 1000:
|
|
|
|
return "%.1fkBps" % (r/1000)
|
2010-07-30 15:05:50 -07:00
|
|
|
return "%.0fBps" % r
|
2008-03-06 21:16:38 -07:00
|
|
|
|
|
|
|
def abbreviate_size(data):
|
|
|
|
# 21.8kB, 554.4kB 4.37MB
|
|
|
|
if data is None:
|
|
|
|
return ""
|
|
|
|
r = float(data)
|
|
|
|
if r > 1000000000:
|
|
|
|
return "%1.2fGB" % (r/1000000000)
|
|
|
|
if r > 1000000:
|
|
|
|
return "%1.2fMB" % (r/1000000)
|
|
|
|
if r > 1000:
|
|
|
|
return "%.1fkB" % (r/1000)
|
2010-07-30 15:05:50 -07:00
|
|
|
return "%.0fB" % r
|
2008-05-19 12:57:04 -07:00
|
|
|
|
2009-03-03 19:40:19 -07:00
|
|
|
def plural(sequence_or_length):
|
|
|
|
if isinstance(sequence_or_length, int):
|
|
|
|
length = sequence_or_length
|
|
|
|
else:
|
|
|
|
length = len(sequence_or_length)
|
|
|
|
if length == 1:
|
|
|
|
return ""
|
|
|
|
return "s"
|
|
|
|
|
2019-09-10 18:47:02 -06:00
|
|
|
def text_plain(text, req):
|
2008-05-19 12:57:04 -07:00
|
|
|
req.setHeader("content-type", "text/plain")
|
2015-01-21 10:31:31 -08:00
|
|
|
req.setHeader("content-length", b"%d" % len(text))
|
2008-05-19 12:57:04 -07:00
|
|
|
return text
|
|
|
|
|
2016-01-04 16:00:59 +00:00
|
|
|
def spaces_to_nbsp(text):
|
|
|
|
return unicode(text).replace(u' ', u'\u00A0')
|
|
|
|
|
2016-01-04 19:58:55 +00:00
|
|
|
def render_time_delta(time_1, time_2):
|
|
|
|
return spaces_to_nbsp(format_delta(time_1, time_2))
|
|
|
|
|
2016-01-04 16:00:59 +00:00
|
|
|
def render_time(t):
|
|
|
|
return spaces_to_nbsp(format_time(time.localtime(t)))
|
|
|
|
|
2016-01-04 19:58:55 +00:00
|
|
|
def render_time_attr(t):
|
|
|
|
return format_time(time.localtime(t))
|
|
|
|
|
2008-05-19 12:57:04 -07:00
|
|
|
|
|
|
|
# XXX: to make UnsupportedMethod return 501 NOT_IMPLEMENTED instead of 500
|
|
|
|
# Internal Server Error, we either need to do that ICanHandleException trick,
|
|
|
|
# or make sure that childFactory returns a WebErrorResource (and never an
|
|
|
|
# actual exception). The latter is growing increasingly annoying.
|
|
|
|
|
|
|
|
def should_create_intermediate_directories(req):
|
|
|
|
t = get_arg(req, "t", "").strip()
|
|
|
|
return bool(req.method in ("PUT", "POST") and
|
|
|
|
t not in ("delete", "rename", "rename-form", "check"))
|
|
|
|
|
2020-09-18 14:49:19 -04:00
|
|
|
def humanize_exception(exc):
|
|
|
|
"""
|
|
|
|
Like ``humanize_failure`` but for an exception.
|
|
|
|
|
|
|
|
:param Exception exc: The exception to describe.
|
|
|
|
|
|
|
|
:return: See ``humanize_failure``.
|
|
|
|
"""
|
|
|
|
if isinstance(exc, EmptyPathnameComponentError):
|
2009-12-27 15:10:43 -05:00
|
|
|
return ("The webapi does not allow empty pathname components, "
|
|
|
|
"i.e. a double slash", http.BAD_REQUEST)
|
2020-09-18 14:49:19 -04:00
|
|
|
if isinstance(exc, ExistingChildError):
|
2009-03-07 04:54:08 -07:00
|
|
|
return ("There was already a child by that name, and you asked me "
|
|
|
|
"to not replace it.", http.CONFLICT)
|
2020-09-18 14:49:19 -04:00
|
|
|
if isinstance(exc, NoSuchChildError):
|
|
|
|
quoted_name = quote_output(exc.args[0], encoding="utf-8", quotemarks=False)
|
2011-01-21 17:40:06 -08:00
|
|
|
return ("No such child: %s" % quoted_name, http.NOT_FOUND)
|
2020-09-18 14:49:19 -04:00
|
|
|
if isinstance(exc, NotEnoughSharesError):
|
2009-06-24 19:17:07 -07:00
|
|
|
t = ("NotEnoughSharesError: This indicates that some "
|
|
|
|
"servers were unavailable, or that shares have been "
|
|
|
|
"lost to server departure, hard drive failure, or disk "
|
|
|
|
"corruption. You should perform a filecheck on "
|
|
|
|
"this object to learn more.\n\nThe full error message is:\n"
|
2020-09-18 14:49:19 -04:00
|
|
|
"%s") % str(exc)
|
2009-06-24 19:17:07 -07:00
|
|
|
return (t, http.GONE)
|
2020-09-18 14:49:19 -04:00
|
|
|
if isinstance(exc, NoSharesError):
|
2009-06-24 19:17:07 -07:00
|
|
|
t = ("NoSharesError: no shares could be found. "
|
|
|
|
"Zero shares usually indicates a corrupt URI, or that "
|
|
|
|
"no servers were connected, but it might also indicate "
|
|
|
|
"severe corruption. You should perform a filecheck on "
|
|
|
|
"this object to learn more.\n\nThe full error message is:\n"
|
2020-09-18 14:49:19 -04:00
|
|
|
"%s") % str(exc)
|
2009-03-07 04:54:08 -07:00
|
|
|
return (t, http.GONE)
|
2020-09-18 14:49:19 -04:00
|
|
|
if isinstance(exc, UnrecoverableFileError):
|
2009-03-07 04:54:08 -07:00
|
|
|
t = ("UnrecoverableFileError: the directory (or mutable file) could "
|
|
|
|
"not be retrieved, because there were insufficient good shares. "
|
|
|
|
"This might indicate that no servers were connected, "
|
|
|
|
"insufficient servers were connected, the URI was corrupt, or "
|
|
|
|
"that shares have been lost due to server departure, hard drive "
|
|
|
|
"failure, or disk corruption. You should perform a filecheck on "
|
|
|
|
"this object to learn more.")
|
|
|
|
return (t, http.GONE)
|
2020-09-18 14:49:19 -04:00
|
|
|
if isinstance(exc, MustNotBeUnknownRWError):
|
|
|
|
quoted_name = quote_output(exc.args[1], encoding="utf-8")
|
|
|
|
immutable = exc.args[2]
|
2010-01-26 22:44:30 -08:00
|
|
|
if immutable:
|
|
|
|
t = ("MustNotBeUnknownRWError: an operation to add a child named "
|
2011-01-21 17:40:06 -08:00
|
|
|
"%s to a directory was given an unknown cap in a write slot.\n"
|
2010-01-26 22:44:30 -08:00
|
|
|
"If the cap is actually an immutable readcap, then using a "
|
|
|
|
"webapi server that supports a later version of Tahoe may help.\n\n"
|
|
|
|
"If you are using the webapi directly, then specifying an immutable "
|
|
|
|
"readcap in the read slot (ro_uri) of the JSON PROPDICT, and "
|
|
|
|
"omitting the write slot (rw_uri), would also work in this "
|
2011-01-21 17:40:06 -08:00
|
|
|
"case.") % quoted_name
|
2010-01-26 22:44:30 -08:00
|
|
|
else:
|
|
|
|
t = ("MustNotBeUnknownRWError: an operation to add a child named "
|
2011-01-21 17:40:06 -08:00
|
|
|
"%s to a directory was given an unknown cap in a write slot.\n"
|
2010-01-26 22:44:30 -08:00
|
|
|
"Using a webapi server that supports a later version of Tahoe "
|
|
|
|
"may help.\n\n"
|
|
|
|
"If you are using the webapi directly, specifying a readcap in "
|
|
|
|
"the read slot (ro_uri) of the JSON PROPDICT, as well as a "
|
|
|
|
"writecap in the write slot if desired, would also work in this "
|
2011-01-21 17:40:06 -08:00
|
|
|
"case.") % quoted_name
|
2010-01-26 22:44:30 -08:00
|
|
|
return (t, http.BAD_REQUEST)
|
2020-09-18 14:49:19 -04:00
|
|
|
if isinstance(exc, MustBeDeepImmutableError):
|
|
|
|
quoted_name = quote_output(exc.args[1], encoding="utf-8")
|
2010-01-26 22:44:30 -08:00
|
|
|
t = ("MustBeDeepImmutableError: a cap passed to this operation for "
|
2011-01-21 17:40:06 -08:00
|
|
|
"the child named %s, needed to be immutable but was not. Either "
|
2010-01-26 22:44:30 -08:00
|
|
|
"the cap is being added to an immutable directory, or it was "
|
|
|
|
"originally retrieved from an immutable directory as an unknown "
|
2011-01-21 17:40:06 -08:00
|
|
|
"cap.") % quoted_name
|
2010-01-26 22:44:30 -08:00
|
|
|
return (t, http.BAD_REQUEST)
|
2020-09-18 14:49:19 -04:00
|
|
|
if isinstance(exc, MustBeReadonlyError):
|
|
|
|
quoted_name = quote_output(exc.args[1], encoding="utf-8")
|
2010-01-26 22:44:30 -08:00
|
|
|
t = ("MustBeReadonlyError: a cap passed to this operation for "
|
|
|
|
"the child named '%s', needed to be read-only but was not. "
|
|
|
|
"The cap is being passed in a read slot (ro_uri), or was retrieved "
|
2011-01-21 17:40:06 -08:00
|
|
|
"from a read slot as an unknown cap.") % quoted_name
|
2009-11-17 23:09:00 -08:00
|
|
|
return (t, http.BAD_REQUEST)
|
2020-09-18 14:49:19 -04:00
|
|
|
if isinstance(exc, blacklist.FileProhibited):
|
|
|
|
t = "Access Prohibited: %s" % quote_output(exc.reason, encoding="utf-8", quotemarks=False)
|
2011-08-24 08:59:28 -07:00
|
|
|
return (t, http.FORBIDDEN)
|
2020-09-18 14:49:19 -04:00
|
|
|
if isinstance(exc, WebError):
|
|
|
|
return (exc.text, exc.code)
|
|
|
|
if isinstance(exc, FileTooLargeError):
|
|
|
|
return ("FileTooLargeError: %s" % (exc,), http.REQUEST_ENTITY_TOO_LARGE)
|
|
|
|
return (str(exc), None)
|
|
|
|
|
2008-05-19 12:57:04 -07:00
|
|
|
|
2020-09-18 14:50:45 -04:00
|
|
|
def humanize_failure(f):
|
|
|
|
"""
|
|
|
|
Create an human-oriented description of a failure along with some HTTP
|
|
|
|
metadata.
|
|
|
|
|
|
|
|
:param Failure f: The failure to describe.
|
|
|
|
|
|
|
|
:return (bytes, int): A tuple of some prose and an HTTP code describing
|
|
|
|
the failure.
|
|
|
|
"""
|
|
|
|
return humanize_exception(f.value)
|
|
|
|
|
2008-05-19 12:57:04 -07:00
|
|
|
|
2020-10-13 09:40:51 -04:00
|
|
|
class MyExceptionHandler(DefaultExceptionHandler, object):
|
2008-05-19 12:57:04 -07:00
|
|
|
def renderHTTP_exception(self, ctx, f):
|
2020-10-16 12:49:36 -04:00
|
|
|
req = INevowRequest(ctx)
|
2020-10-15 11:43:42 -04:00
|
|
|
req.write(_renderHTTP_exception(req, f))
|
|
|
|
req.finishRequest(False)
|
2008-05-19 15:19:25 -07:00
|
|
|
|
2016-01-26 23:49:23 -07:00
|
|
|
|
2008-10-21 17:03:07 -07:00
|
|
|
class NeedOperationHandleError(WebError):
|
|
|
|
pass
|
|
|
|
|
2016-01-26 23:49:23 -07:00
|
|
|
|
2019-08-14 10:37:56 +01:00
|
|
|
class SlotsSequenceElement(template.Element):
|
2019-08-14 20:17:11 +01:00
|
|
|
"""
|
2019-08-16 17:36:48 +01:00
|
|
|
``SlotsSequenceElement` is a minimal port of nevow's sequence renderer for
|
|
|
|
twisted.web.template.
|
2019-08-14 20:17:11 +01:00
|
|
|
|
2019-08-16 17:36:48 +01:00
|
|
|
Tags passed in to be templated will have two renderers available: ``item``
|
|
|
|
and ``tag``.
|
2019-08-14 20:17:11 +01:00
|
|
|
"""
|
|
|
|
|
2019-08-14 10:37:56 +01:00
|
|
|
def __init__(self, tag, seq):
|
|
|
|
self.loader = template.TagLoader(tag)
|
|
|
|
self.seq = seq
|
|
|
|
|
2019-09-03 17:59:45 -06:00
|
|
|
@template.renderer
|
|
|
|
def header(self, request, tag):
|
|
|
|
return tag
|
|
|
|
|
2019-08-14 10:37:56 +01:00
|
|
|
@template.renderer
|
|
|
|
def item(self, request, tag):
|
2019-08-16 17:36:48 +01:00
|
|
|
"""
|
|
|
|
A template renderer for each sequence item.
|
|
|
|
|
|
|
|
``tag`` will be cloned for each item in the sequence provided, and its
|
|
|
|
slots filled from the sequence item. Each item must be dict-like enough
|
|
|
|
for ``tag.fillSlots(**item)``. Each cloned tag will be siblings with no
|
|
|
|
separator beween them.
|
|
|
|
"""
|
2019-08-14 10:37:56 +01:00
|
|
|
for item in self.seq:
|
2019-08-14 20:17:11 +01:00
|
|
|
yield tag.clone(deep=False).fillSlots(**item)
|
2019-08-14 10:37:56 +01:00
|
|
|
|
2019-08-14 20:17:11 +01:00
|
|
|
@template.renderer
|
2019-08-14 10:37:56 +01:00
|
|
|
def empty(self, request, tag):
|
2019-08-16 17:36:48 +01:00
|
|
|
"""
|
|
|
|
A template renderer for empty sequences.
|
|
|
|
|
|
|
|
This renderer will either return ``tag`` unmodified if the provided
|
|
|
|
sequence has no items, or return the empty string if there are any
|
|
|
|
items.
|
|
|
|
"""
|
2019-08-14 10:37:56 +01:00
|
|
|
if len(self.seq) > 0:
|
2019-08-16 16:51:57 +01:00
|
|
|
return u''
|
2019-08-14 10:37:56 +01:00
|
|
|
else:
|
|
|
|
return tag
|
|
|
|
|
|
|
|
|
2020-10-15 16:14:06 -04:00
|
|
|
def exception_to_child(getChild):
|
2020-09-18 14:49:19 -04:00
|
|
|
"""
|
|
|
|
Decorate ``getChild`` method with exception handling behavior to render an
|
|
|
|
error page reflecting the exception.
|
|
|
|
"""
|
2020-10-15 16:14:06 -04:00
|
|
|
@wraps(getChild)
|
2020-09-18 14:49:19 -04:00
|
|
|
def g(self, name, req):
|
2020-10-21 07:34:27 -04:00
|
|
|
# Bind the method to the instance so it has a better
|
|
|
|
# fullyQualifiedName later on. This is not necessary on Python 3.
|
2020-10-15 16:14:06 -04:00
|
|
|
bound_getChild = getChild.__get__(self, type(self))
|
2020-10-16 09:30:09 -04:00
|
|
|
|
|
|
|
action = start_action(
|
|
|
|
action_type=u"allmydata:web:common-getChild",
|
|
|
|
uri=req.uri,
|
|
|
|
method=req.method,
|
|
|
|
name=name,
|
|
|
|
handler=fullyQualifiedName(bound_getChild),
|
2020-10-15 16:14:06 -04:00
|
|
|
)
|
2020-10-16 09:30:09 -04:00
|
|
|
with action.context():
|
|
|
|
result = DeferredContext(maybeDeferred(bound_getChild, name, req))
|
|
|
|
result.addCallbacks(
|
|
|
|
_getChild_done,
|
|
|
|
_getChild_failed,
|
|
|
|
callbackArgs=(self,),
|
|
|
|
)
|
|
|
|
result = result.addActionFinish()
|
2020-10-15 16:14:06 -04:00
|
|
|
return DeferredResource(result)
|
2020-09-18 14:49:19 -04:00
|
|
|
return g
|
|
|
|
|
|
|
|
|
2020-10-15 16:14:06 -04:00
|
|
|
def _getChild_done(child, parent):
|
2020-10-16 09:30:09 -04:00
|
|
|
Message.log(
|
|
|
|
message_type=u"allmydata:web:common-getChild:result",
|
|
|
|
result=fullyQualifiedName(type(child)),
|
|
|
|
)
|
2020-10-15 16:14:06 -04:00
|
|
|
if child is None:
|
|
|
|
return resource.NoResource()
|
|
|
|
return child
|
|
|
|
|
|
|
|
|
|
|
|
def _getChild_failed(reason):
|
|
|
|
text, code = humanize_failure(reason)
|
|
|
|
return resource.ErrorPage(code, "Error", text)
|
|
|
|
|
|
|
|
|
2020-10-15 11:43:42 -04:00
|
|
|
def render_exception(render):
|
2020-09-18 14:49:19 -04:00
|
|
|
"""
|
|
|
|
Decorate a ``render_*`` method with exception handling behavior to render
|
|
|
|
an error page reflecting the exception.
|
|
|
|
"""
|
2020-10-15 11:43:42 -04:00
|
|
|
@wraps(render)
|
2020-09-18 14:49:19 -04:00
|
|
|
def g(self, request):
|
2020-10-21 07:34:27 -04:00
|
|
|
# Bind the method to the instance so it has a better
|
|
|
|
# fullyQualifiedName later on. This is not necessary on Python 3.
|
2020-10-15 11:43:42 -04:00
|
|
|
bound_render = render.__get__(self, type(self))
|
2020-10-16 09:30:09 -04:00
|
|
|
|
|
|
|
action = start_action(
|
|
|
|
action_type=u"allmydata:web:common-render",
|
|
|
|
uri=request.uri,
|
|
|
|
method=request.method,
|
|
|
|
handler=fullyQualifiedName(bound_render),
|
|
|
|
)
|
2020-10-16 09:30:22 -04:00
|
|
|
if getattr(request, "dont_apply_extra_processing", False):
|
|
|
|
with action:
|
|
|
|
return bound_render(request)
|
|
|
|
|
2020-10-16 09:30:09 -04:00
|
|
|
with action.context():
|
|
|
|
result = DeferredContext(maybeDeferred(bound_render, request))
|
2020-10-21 07:25:48 -04:00
|
|
|
# Apply `_finish` all of our result handling logic to whatever it
|
|
|
|
# returned.
|
2020-10-16 09:30:09 -04:00
|
|
|
result.addBoth(_finish, bound_render, request)
|
|
|
|
result.addActionFinish()
|
2020-10-15 16:02:32 -04:00
|
|
|
return NOT_DONE_YET
|
2020-10-15 11:43:42 -04:00
|
|
|
|
2020-09-18 14:49:19 -04:00
|
|
|
return g
|
2020-10-15 11:43:42 -04:00
|
|
|
|
|
|
|
|
|
|
|
def _finish(result, render, request):
|
2020-10-21 07:18:41 -04:00
|
|
|
"""
|
|
|
|
Try to finish rendering the response to a request.
|
|
|
|
|
|
|
|
This implements extra convenience functionality not provided by Twisted
|
|
|
|
Web. Various resources in Tahoe-LAFS made use of this functionality when
|
|
|
|
it was provided by Nevow. Rather than making that application code do the
|
|
|
|
more tedious thing itself, we duplicate the functionality here.
|
|
|
|
|
|
|
|
:param result: Something returned by a render method which we can turn
|
|
|
|
into a response.
|
|
|
|
|
|
|
|
:param render: The original render method which produced the result.
|
|
|
|
|
|
|
|
:param request: The request being responded to.
|
|
|
|
|
|
|
|
:return: ``None``
|
|
|
|
"""
|
2020-10-15 11:43:42 -04:00
|
|
|
if isinstance(result, Failure):
|
2020-10-16 09:30:09 -04:00
|
|
|
Message.log(
|
|
|
|
message_type=u"allmydata:web:common-render:failure",
|
|
|
|
message=result.getErrorMessage(),
|
|
|
|
)
|
2020-10-15 11:43:42 -04:00
|
|
|
_finish(
|
|
|
|
_renderHTTP_exception(request, result),
|
|
|
|
render,
|
|
|
|
request,
|
|
|
|
)
|
|
|
|
elif IResource.providedBy(result):
|
|
|
|
# If result is also using @render_exception then we don't want to
|
|
|
|
# double-apply the logic. This leads to an attempt to double-finish
|
|
|
|
# the request. If it isn't using @render_exception then you should
|
|
|
|
# fix it so it is.
|
2020-10-16 09:30:09 -04:00
|
|
|
Message.log(
|
|
|
|
message_type=u"allmydata:web:common-render:resource",
|
|
|
|
resource=fullyQualifiedName(type(result)),
|
|
|
|
)
|
2020-10-15 11:43:42 -04:00
|
|
|
result.render(request)
|
2020-10-18 10:58:09 -04:00
|
|
|
elif isinstance(result, unicode):
|
|
|
|
Message.log(
|
|
|
|
message_type=u"allmydata:web:common-render:unicode",
|
|
|
|
)
|
|
|
|
request.write(result.encode("utf-8"))
|
|
|
|
request.finish()
|
2020-10-15 11:43:42 -04:00
|
|
|
elif isinstance(result, bytes):
|
2020-10-16 09:30:09 -04:00
|
|
|
Message.log(
|
|
|
|
message_type=u"allmydata:web:common-render:bytes",
|
|
|
|
)
|
2020-10-15 11:43:42 -04:00
|
|
|
request.write(result)
|
|
|
|
request.finish()
|
|
|
|
elif isinstance(result, DecodedURL):
|
2020-10-16 09:30:09 -04:00
|
|
|
Message.log(
|
|
|
|
message_type=u"allmydata:web:common-render:DecodedURL",
|
|
|
|
)
|
2020-10-15 11:43:42 -04:00
|
|
|
_finish(redirectTo(str(result), request), render, request)
|
|
|
|
elif result is None:
|
2020-10-16 09:30:09 -04:00
|
|
|
Message.log(
|
|
|
|
message_type=u"allmydata:web:common-render:None",
|
|
|
|
)
|
2020-10-15 11:43:42 -04:00
|
|
|
request.finish()
|
2020-10-15 15:15:54 -04:00
|
|
|
elif result == NOT_DONE_YET:
|
2020-10-16 09:30:09 -04:00
|
|
|
Message.log(
|
|
|
|
message_type=u"allmydata:web:common-render:NOT_DONE_YET",
|
|
|
|
)
|
2020-10-15 11:43:42 -04:00
|
|
|
pass
|
|
|
|
else:
|
2020-10-16 09:30:09 -04:00
|
|
|
Message.log(
|
|
|
|
message_type=u"allmydata:web:common-render:unknown",
|
|
|
|
)
|
2020-10-15 11:43:42 -04:00
|
|
|
log.err("Request for {!r} handled by {!r} returned unusable {!r}".format(
|
|
|
|
request.uri,
|
|
|
|
fullyQualifiedName(render),
|
|
|
|
result,
|
|
|
|
))
|
|
|
|
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
|
2020-10-19 14:26:12 -04:00
|
|
|
_finish(b"Internal Server Error", render, request)
|
2020-10-15 11:43:42 -04:00
|
|
|
|
|
|
|
|
|
|
|
def _renderHTTP_exception(request, failure):
|
|
|
|
try:
|
|
|
|
text, code = humanize_failure(failure)
|
|
|
|
except:
|
|
|
|
log.msg("exception in humanize_failure")
|
|
|
|
log.msg("argument was %s" % (failure,))
|
|
|
|
log.err()
|
|
|
|
text = str(failure)
|
|
|
|
code = None
|
|
|
|
|
|
|
|
if code is not None:
|
|
|
|
return _renderHTTP_exception_simple(request, text, code)
|
|
|
|
|
2020-10-15 15:15:54 -04:00
|
|
|
if failure.check(UnsupportedMethod):
|
2020-10-15 11:43:42 -04:00
|
|
|
# twisted.web.server.Request.render() has support for transforming
|
|
|
|
# this into an appropriate 501 NOT_IMPLEMENTED or 405 NOT_ALLOWED
|
|
|
|
# return code, but nevow does not.
|
|
|
|
method = request.method
|
|
|
|
return _renderHTTP_exception_simple(
|
|
|
|
request,
|
|
|
|
"I don't know how to treat a %s request." % (method,),
|
|
|
|
http.NOT_IMPLEMENTED,
|
|
|
|
)
|
|
|
|
|
|
|
|
accept = request.getHeader("accept")
|
|
|
|
if not accept:
|
|
|
|
accept = "*/*"
|
|
|
|
if "*/*" in accept or "text/*" in accept or "text/html" in accept:
|
|
|
|
request.setResponseCode(http.INTERNAL_SERVER_ERROR)
|
|
|
|
return template.renderElement(
|
|
|
|
request,
|
|
|
|
tags.html(
|
|
|
|
tags.head(
|
|
|
|
tags.title(u"Exception"),
|
|
|
|
),
|
|
|
|
tags.body(
|
|
|
|
FailureElement(failure),
|
|
|
|
),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
|
|
|
# use plain text
|
|
|
|
traceback = failure.getTraceback()
|
|
|
|
return _renderHTTP_exception_simple(
|
|
|
|
request,
|
|
|
|
traceback,
|
|
|
|
http.INTERNAL_SERVER_ERROR,
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
def _renderHTTP_exception_simple(request, text, code):
|
|
|
|
request.setResponseCode(code)
|
|
|
|
request.setHeader("content-type", "text/plain;charset=utf-8")
|
|
|
|
if isinstance(text, unicode):
|
|
|
|
text = text.encode("utf-8")
|
|
|
|
request.setHeader("content-length", b"%d" % len(text))
|
|
|
|
return text
|
|
|
|
|
|
|
|
|
|
|
|
def handle_when_done(req, d):
|
|
|
|
when_done = get_arg(req, "when_done", None)
|
|
|
|
if when_done:
|
|
|
|
d.addCallback(lambda res: DecodedURL.from_text(when_done.decode("utf-8")))
|
|
|
|
return d
|
2020-10-19 12:33:15 -04:00
|
|
|
|
|
|
|
|
|
|
|
def url_for_string(req, url_string):
|
|
|
|
"""
|
|
|
|
Construct a universal URL using the given URL string.
|
|
|
|
|
|
|
|
:param IRequest req: The request being served. If ``redir_to`` is not
|
|
|
|
absolute then this is used to determine the net location of this
|
|
|
|
server and the resulting URL is made to point at it.
|
|
|
|
|
|
|
|
:param bytes url_string: A byte string giving a universal or absolute URL.
|
|
|
|
|
|
|
|
:return DecodedURL: An absolute URL based on this server's net location
|
|
|
|
and the given URL string.
|
|
|
|
"""
|
|
|
|
url = DecodedURL.from_text(url_string.decode("utf-8"))
|
|
|
|
if url.host == b"":
|
|
|
|
root = req.URLPath()
|
|
|
|
netloc = root.netloc.split(b":", 1)
|
|
|
|
if len(netloc) == 1:
|
|
|
|
host = netloc
|
|
|
|
port = None
|
|
|
|
else:
|
|
|
|
host = netloc[0]
|
|
|
|
port = int(netloc[1])
|
|
|
|
url = url.replace(
|
|
|
|
scheme=root.scheme.decode("ascii"),
|
|
|
|
host=host.decode("ascii"),
|
|
|
|
port=port,
|
|
|
|
)
|
|
|
|
return url
|