Remove base_prefix and use SCRIPT_NAME instead
This conforms with the WSGI reference (PEP 333)
This commit is contained in:
parent
b85fc5bed6
commit
dbaf58dbfe
6
config
6
config
@ -51,12 +51,6 @@
|
|||||||
# Reverse DNS to resolve client address in logs
|
# Reverse DNS to resolve client address in logs
|
||||||
#dns_lookup = True
|
#dns_lookup = True
|
||||||
|
|
||||||
# Root URL of Radicale (starting and ending with a slash)
|
|
||||||
#base_prefix = /
|
|
||||||
|
|
||||||
# Possibility to allow URLs cleaned by a HTTP server, without the base_prefix
|
|
||||||
#can_skip_base_prefix = False
|
|
||||||
|
|
||||||
# Message displayed in the client when a password is needed
|
# Message displayed in the client when a password is needed
|
||||||
#realm = Radicale - Password Required
|
#realm = Radicale - Password Required
|
||||||
|
|
||||||
|
@ -307,19 +307,11 @@ class Application:
|
|||||||
headers = pprint.pformat(self.headers_log(environ))
|
headers = pprint.pformat(self.headers_log(environ))
|
||||||
self.logger.debug("Request headers:\n%s", headers)
|
self.logger.debug("Request headers:\n%s", headers)
|
||||||
|
|
||||||
# Strip base_prefix from request URI
|
# Sanitize base prefix
|
||||||
base_prefix = self.configuration.get("server", "base_prefix")
|
environ["SCRIPT_NAME"] = storage.sanitize_path(
|
||||||
if environ["PATH_INFO"].startswith(base_prefix):
|
environ.get("SCRIPT_NAME", "")).rstrip("/")
|
||||||
environ["PATH_INFO"] = environ["PATH_INFO"][len(base_prefix):]
|
self.logger.debug("Sanitized script name: %s", environ["SCRIPT_NAME"])
|
||||||
elif self.configuration.get("server", "can_skip_base_prefix"):
|
base_prefix = environ["SCRIPT_NAME"]
|
||||||
self.logger.debug(
|
|
||||||
"Prefix already stripped from path: %s", environ["PATH_INFO"])
|
|
||||||
else:
|
|
||||||
# Request path not starting with base_prefix, not allowed
|
|
||||||
self.logger.debug(
|
|
||||||
"Path not starting with prefix: %s", environ["PATH_INFO"])
|
|
||||||
return response(*NOT_ALLOWED)
|
|
||||||
|
|
||||||
# Sanitize request URI
|
# Sanitize request URI
|
||||||
environ["PATH_INFO"] = storage.sanitize_path(
|
environ["PATH_INFO"] = storage.sanitize_path(
|
||||||
unquote(environ["PATH_INFO"]))
|
unquote(environ["PATH_INFO"]))
|
||||||
@ -376,7 +368,8 @@ class Application:
|
|||||||
|
|
||||||
if is_valid_user:
|
if is_valid_user:
|
||||||
try:
|
try:
|
||||||
status, headers, answer = function(environ, path, user)
|
status, headers, answer = function(
|
||||||
|
environ, base_prefix, path, user)
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
return response(*REQUEST_TIMEOUT)
|
return response(*REQUEST_TIMEOUT)
|
||||||
else:
|
else:
|
||||||
@ -423,7 +416,7 @@ class Application:
|
|||||||
content = None
|
content = None
|
||||||
return content
|
return content
|
||||||
|
|
||||||
def do_DELETE(self, environ, path, user):
|
def do_DELETE(self, environ, base_prefix, path, user):
|
||||||
"""Manage DELETE request."""
|
"""Manage DELETE request."""
|
||||||
if not self._access(user, path, "w"):
|
if not self._access(user, path, "w"):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
@ -438,12 +431,13 @@ class Application:
|
|||||||
# ETag precondition not verified, do not delete item
|
# ETag precondition not verified, do not delete item
|
||||||
return PRECONDITION_FAILED
|
return PRECONDITION_FAILED
|
||||||
if isinstance(item, self.Collection):
|
if isinstance(item, self.Collection):
|
||||||
answer = xmlutils.delete(path, item)
|
answer = xmlutils.delete(base_prefix, path, item)
|
||||||
else:
|
else:
|
||||||
answer = xmlutils.delete(path, item.collection, item.href)
|
answer = xmlutils.delete(
|
||||||
|
base_prefix, path, item.collection, item.href)
|
||||||
return client.OK, {}, answer
|
return client.OK, {}, answer
|
||||||
|
|
||||||
def do_GET(self, environ, path, user):
|
def do_GET(self, environ, base_prefix, path, user):
|
||||||
"""Manage GET request."""
|
"""Manage GET request."""
|
||||||
# Display a "Radicale works!" message if the root URL is requested
|
# Display a "Radicale works!" message if the root URL is requested
|
||||||
if not path.strip("/"):
|
if not path.strip("/"):
|
||||||
@ -471,12 +465,13 @@ class Application:
|
|||||||
answer = item.serialize()
|
answer = item.serialize()
|
||||||
return client.OK, headers, answer
|
return client.OK, headers, answer
|
||||||
|
|
||||||
def do_HEAD(self, environ, path, user):
|
def do_HEAD(self, environ, base_prefix, path, user):
|
||||||
"""Manage HEAD request."""
|
"""Manage HEAD request."""
|
||||||
status, headers, answer = self.do_GET(environ, path, user)
|
status, headers, answer = self.do_GET(
|
||||||
|
environ, base_prefix, path, user)
|
||||||
return status, headers, None
|
return status, headers, None
|
||||||
|
|
||||||
def do_MKCALENDAR(self, environ, path, user):
|
def do_MKCALENDAR(self, environ, base_prefix, path, user):
|
||||||
"""Manage MKCALENDAR request."""
|
"""Manage MKCALENDAR request."""
|
||||||
if not self.authorized(user, path, "w"):
|
if not self.authorized(user, path, "w"):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
@ -492,7 +487,7 @@ class Application:
|
|||||||
self.Collection.create_collection(path, props=props)
|
self.Collection.create_collection(path, props=props)
|
||||||
return client.CREATED, {}, None
|
return client.CREATED, {}, None
|
||||||
|
|
||||||
def do_MKCOL(self, environ, path, user):
|
def do_MKCOL(self, environ, base_prefix, path, user):
|
||||||
"""Manage MKCOL request."""
|
"""Manage MKCOL request."""
|
||||||
if not self.authorized(user, path, "w"):
|
if not self.authorized(user, path, "w"):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
@ -505,7 +500,7 @@ class Application:
|
|||||||
self.Collection.create_collection(path, props=props)
|
self.Collection.create_collection(path, props=props)
|
||||||
return client.CREATED, {}, None
|
return client.CREATED, {}, None
|
||||||
|
|
||||||
def do_MOVE(self, environ, path, user):
|
def do_MOVE(self, environ, base_prefix, path, user):
|
||||||
"""Manage MOVE request."""
|
"""Manage MOVE request."""
|
||||||
to_url = urlparse(environ["HTTP_DESTINATION"])
|
to_url = urlparse(environ["HTTP_DESTINATION"])
|
||||||
if to_url.netloc != environ["HTTP_HOST"]:
|
if to_url.netloc != environ["HTTP_HOST"]:
|
||||||
@ -542,7 +537,7 @@ class Application:
|
|||||||
self.Collection.move(item, to_collection, to_href)
|
self.Collection.move(item, to_collection, to_href)
|
||||||
return client.CREATED, {}, None
|
return client.CREATED, {}, None
|
||||||
|
|
||||||
def do_OPTIONS(self, environ, path, user):
|
def do_OPTIONS(self, environ, base_prefix, path, user):
|
||||||
"""Manage OPTIONS request."""
|
"""Manage OPTIONS request."""
|
||||||
headers = {
|
headers = {
|
||||||
"Allow": ", ".join(
|
"Allow": ", ".join(
|
||||||
@ -550,7 +545,7 @@ class Application:
|
|||||||
"DAV": DAV_HEADERS}
|
"DAV": DAV_HEADERS}
|
||||||
return client.OK, headers, None
|
return client.OK, headers, None
|
||||||
|
|
||||||
def do_PROPFIND(self, environ, path, user):
|
def do_PROPFIND(self, environ, base_prefix, path, user):
|
||||||
"""Manage PROPFIND request."""
|
"""Manage PROPFIND request."""
|
||||||
if not self._access(user, path, "r"):
|
if not self._access(user, path, "r"):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
@ -569,13 +564,13 @@ class Application:
|
|||||||
read_items, write_items = self.collect_allowed_items(items, user)
|
read_items, write_items = self.collect_allowed_items(items, user)
|
||||||
headers = {"DAV": DAV_HEADERS, "Content-Type": "text/xml"}
|
headers = {"DAV": DAV_HEADERS, "Content-Type": "text/xml"}
|
||||||
status, answer = xmlutils.propfind(
|
status, answer = xmlutils.propfind(
|
||||||
path, content, read_items, write_items, user)
|
base_prefix, path, content, read_items, write_items, user)
|
||||||
if status == client.FORBIDDEN:
|
if status == client.FORBIDDEN:
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
else:
|
else:
|
||||||
return status, headers, answer
|
return status, headers, answer
|
||||||
|
|
||||||
def do_PROPPATCH(self, environ, path, user):
|
def do_PROPPATCH(self, environ, base_prefix, path, user):
|
||||||
"""Manage PROPPATCH request."""
|
"""Manage PROPPATCH request."""
|
||||||
if not self.authorized(user, path, "w"):
|
if not self.authorized(user, path, "w"):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
@ -585,10 +580,10 @@ class Application:
|
|||||||
if not isinstance(item, self.Collection):
|
if not isinstance(item, self.Collection):
|
||||||
return WEBDAV_PRECONDITION_FAILED
|
return WEBDAV_PRECONDITION_FAILED
|
||||||
headers = {"DAV": DAV_HEADERS, "Content-Type": "text/xml"}
|
headers = {"DAV": DAV_HEADERS, "Content-Type": "text/xml"}
|
||||||
answer = xmlutils.proppatch(path, content, item)
|
answer = xmlutils.proppatch(base_prefix, path, content, item)
|
||||||
return client.MULTI_STATUS, headers, answer
|
return client.MULTI_STATUS, headers, answer
|
||||||
|
|
||||||
def do_PUT(self, environ, path, user):
|
def do_PUT(self, environ, base_prefix, path, user):
|
||||||
"""Manage PUT request."""
|
"""Manage PUT request."""
|
||||||
if not self._access(user, path, "w"):
|
if not self._access(user, path, "w"):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
@ -640,7 +635,7 @@ class Application:
|
|||||||
headers = {"ETag": new_item.etag}
|
headers = {"ETag": new_item.etag}
|
||||||
return client.CREATED, headers, None
|
return client.CREATED, headers, None
|
||||||
|
|
||||||
def do_REPORT(self, environ, path, user):
|
def do_REPORT(self, environ, base_prefix, path, user):
|
||||||
"""Manage REPORT request."""
|
"""Manage REPORT request."""
|
||||||
if not self._access(user, path, "w"):
|
if not self._access(user, path, "w"):
|
||||||
return NOT_ALLOWED
|
return NOT_ALLOWED
|
||||||
@ -656,5 +651,5 @@ class Application:
|
|||||||
else:
|
else:
|
||||||
collection = item.collection
|
collection = item.collection
|
||||||
headers = {"Content-Type": "text/xml"}
|
headers = {"Content-Type": "text/xml"}
|
||||||
answer = xmlutils.report(path, content, collection)
|
answer = xmlutils.report(base_prefix, path, content, collection)
|
||||||
return client.MULTI_STATUS, headers, answer
|
return client.MULTI_STATUS, headers, answer
|
||||||
|
@ -153,9 +153,6 @@ def serve(configuration, logger):
|
|||||||
atexit.register(cleanup)
|
atexit.register(cleanup)
|
||||||
logger.info("Starting Radicale")
|
logger.info("Starting Radicale")
|
||||||
|
|
||||||
logger.debug(
|
|
||||||
"Base URL prefix: %s", configuration.get("server", "base_prefix"))
|
|
||||||
|
|
||||||
# Create collection servers
|
# Create collection servers
|
||||||
servers = {}
|
servers = {}
|
||||||
if configuration.getboolean("server", "ssl"):
|
if configuration.getboolean("server", "ssl"):
|
||||||
|
@ -41,8 +41,6 @@ INITIAL_CONFIG = {
|
|||||||
"protocol": "PROTOCOL_SSLv23",
|
"protocol": "PROTOCOL_SSLv23",
|
||||||
"ciphers": "",
|
"ciphers": "",
|
||||||
"dns_lookup": "True",
|
"dns_lookup": "True",
|
||||||
"base_prefix": "/",
|
|
||||||
"can_skip_base_prefix": "False",
|
|
||||||
"realm": "Radicale - Password Required"},
|
"realm": "Radicale - Password Required"},
|
||||||
"encoding": {
|
"encoding": {
|
||||||
"request": "utf-8",
|
"request": "utf-8",
|
||||||
|
@ -102,11 +102,9 @@ def _response(code):
|
|||||||
return "HTTP/1.1 %i %s" % (code, client.responses[code])
|
return "HTTP/1.1 %i %s" % (code, client.responses[code])
|
||||||
|
|
||||||
|
|
||||||
def _href(collection, href):
|
def _href(base_prefix, href):
|
||||||
"""Return prefixed href."""
|
"""Return prefixed href."""
|
||||||
return "%s%s" % (
|
return "%s%s" % (base_prefix, href)
|
||||||
collection.configuration.get("server", "base_prefix"),
|
|
||||||
href.lstrip("/"))
|
|
||||||
|
|
||||||
|
|
||||||
def _date_to_datetime(date_):
|
def _date_to_datetime(date_):
|
||||||
@ -466,7 +464,7 @@ def props_from_request(root, actions=("set", "remove")):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def delete(path, collection, href=None):
|
def delete(base_prefix, path, collection, href=None):
|
||||||
"""Read and answer DELETE requests.
|
"""Read and answer DELETE requests.
|
||||||
|
|
||||||
Read rfc4918-9.6 for info.
|
Read rfc4918-9.6 for info.
|
||||||
@ -479,7 +477,7 @@ def delete(path, collection, href=None):
|
|||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
|
|
||||||
href = ET.Element(_tag("D", "href"))
|
href = ET.Element(_tag("D", "href"))
|
||||||
href.text = _href(collection, path)
|
href.text = _href(base_prefix, path)
|
||||||
response.append(href)
|
response.append(href)
|
||||||
|
|
||||||
status = ET.Element(_tag("D", "status"))
|
status = ET.Element(_tag("D", "status"))
|
||||||
@ -489,7 +487,8 @@ def delete(path, collection, href=None):
|
|||||||
return _pretty_xml(multistatus)
|
return _pretty_xml(multistatus)
|
||||||
|
|
||||||
|
|
||||||
def propfind(path, xml_request, read_collections, write_collections, user):
|
def propfind(base_prefix, path, xml_request, read_collections,
|
||||||
|
write_collections, user):
|
||||||
"""Read and answer PROPFIND requests.
|
"""Read and answer PROPFIND requests.
|
||||||
|
|
||||||
Read rfc4918-9.1 for info.
|
Read rfc4918-9.1 for info.
|
||||||
@ -522,19 +521,19 @@ def propfind(path, xml_request, read_collections, write_collections, user):
|
|||||||
for collection in write_collections:
|
for collection in write_collections:
|
||||||
collections.append(collection)
|
collections.append(collection)
|
||||||
response = _propfind_response(
|
response = _propfind_response(
|
||||||
path, collection, props, user, write=True)
|
base_prefix, path, collection, props, user, write=True)
|
||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
for collection in read_collections:
|
for collection in read_collections:
|
||||||
if collection in collections:
|
if collection in collections:
|
||||||
continue
|
continue
|
||||||
response = _propfind_response(
|
response = _propfind_response(
|
||||||
path, collection, props, user, write=False)
|
base_prefix, path, collection, props, user, write=False)
|
||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
|
|
||||||
return client.MULTI_STATUS, _pretty_xml(multistatus)
|
return client.MULTI_STATUS, _pretty_xml(multistatus)
|
||||||
|
|
||||||
|
|
||||||
def _propfind_response(path, item, props, user, write=False):
|
def _propfind_response(base_prefix, path, item, props, user, write=False):
|
||||||
"""Build and return a PROPFIND response."""
|
"""Build and return a PROPFIND response."""
|
||||||
is_collection = isinstance(item, storage.BaseCollection)
|
is_collection = isinstance(item, storage.BaseCollection)
|
||||||
if is_collection:
|
if is_collection:
|
||||||
@ -552,7 +551,7 @@ def _propfind_response(path, item, props, user, write=False):
|
|||||||
else:
|
else:
|
||||||
uri = "/" + posixpath.join(collection.path, item.href)
|
uri = "/" + posixpath.join(collection.path, item.href)
|
||||||
|
|
||||||
href.text = _href(collection, uri)
|
href.text = _href(base_prefix, uri)
|
||||||
response.append(href)
|
response.append(href)
|
||||||
|
|
||||||
propstat404 = ET.Element(_tag("D", "propstat"))
|
propstat404 = ET.Element(_tag("D", "propstat"))
|
||||||
@ -574,7 +573,7 @@ def _propfind_response(path, item, props, user, write=False):
|
|||||||
element.text = item.last_modified
|
element.text = item.last_modified
|
||||||
elif tag == _tag("D", "principal-collection-set"):
|
elif tag == _tag("D", "principal-collection-set"):
|
||||||
tag = ET.Element(_tag("D", "href"))
|
tag = ET.Element(_tag("D", "href"))
|
||||||
tag.text = _href(collection, "/")
|
tag.text = _href(base_prefix, "/")
|
||||||
element.append(tag)
|
element.append(tag)
|
||||||
elif (tag in (_tag("C", "calendar-user-address-set"),
|
elif (tag in (_tag("C", "calendar-user-address-set"),
|
||||||
_tag("D", "principal-URL"),
|
_tag("D", "principal-URL"),
|
||||||
@ -582,7 +581,7 @@ def _propfind_response(path, item, props, user, write=False):
|
|||||||
_tag("C", "calendar-home-set")) and
|
_tag("C", "calendar-home-set")) and
|
||||||
collection.is_principal and is_collection):
|
collection.is_principal and is_collection):
|
||||||
tag = ET.Element(_tag("D", "href"))
|
tag = ET.Element(_tag("D", "href"))
|
||||||
tag.text = _href(collection, path)
|
tag.text = _href(base_prefix, path)
|
||||||
element.append(tag)
|
element.append(tag)
|
||||||
elif tag == _tag("C", "supported-calendar-component-set"):
|
elif tag == _tag("C", "supported-calendar-component-set"):
|
||||||
human_tag = _tag_from_clark(tag)
|
human_tag = _tag_from_clark(tag)
|
||||||
@ -600,7 +599,7 @@ def _propfind_response(path, item, props, user, write=False):
|
|||||||
is404 = True
|
is404 = True
|
||||||
elif tag == _tag("D", "current-user-principal"):
|
elif tag == _tag("D", "current-user-principal"):
|
||||||
tag = ET.Element(_tag("D", "href"))
|
tag = ET.Element(_tag("D", "href"))
|
||||||
tag.text = _href(collection, ("/%s/" % user) if user else "/")
|
tag.text = _href(base_prefix, ("/%s/" % user) if user else "/")
|
||||||
element.append(tag)
|
element.append(tag)
|
||||||
elif tag == _tag("D", "current-user-privilege-set"):
|
elif tag == _tag("D", "current-user-privilege-set"):
|
||||||
privilege = ET.Element(_tag("D", "privilege"))
|
privilege = ET.Element(_tag("D", "privilege"))
|
||||||
@ -720,7 +719,7 @@ def _add_propstat_to(element, tag, status_number):
|
|||||||
propstat.append(status)
|
propstat.append(status)
|
||||||
|
|
||||||
|
|
||||||
def proppatch(path, xml_request, collection):
|
def proppatch(base_prefix, path, xml_request, collection):
|
||||||
"""Read and answer PROPPATCH requests.
|
"""Read and answer PROPPATCH requests.
|
||||||
|
|
||||||
Read rfc4918-9.2 for info.
|
Read rfc4918-9.2 for info.
|
||||||
@ -735,7 +734,7 @@ def proppatch(path, xml_request, collection):
|
|||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
|
|
||||||
href = ET.Element(_tag("D", "href"))
|
href = ET.Element(_tag("D", "href"))
|
||||||
href.text = _href(collection, path)
|
href.text = _href(base_prefix, path)
|
||||||
response.append(href)
|
response.append(href)
|
||||||
|
|
||||||
for short_name in props_to_remove:
|
for short_name in props_to_remove:
|
||||||
@ -748,7 +747,7 @@ def proppatch(path, xml_request, collection):
|
|||||||
return _pretty_xml(multistatus)
|
return _pretty_xml(multistatus)
|
||||||
|
|
||||||
|
|
||||||
def report(path, xml_request, collection):
|
def report(base_prefix, path, xml_request, collection):
|
||||||
"""Read and answer REPORT requests.
|
"""Read and answer REPORT requests.
|
||||||
|
|
||||||
Read rfc3253-3.6 for info.
|
Read rfc3253-3.6 for info.
|
||||||
@ -765,12 +764,11 @@ def report(path, xml_request, collection):
|
|||||||
_tag("C", "calendar-multiget"),
|
_tag("C", "calendar-multiget"),
|
||||||
_tag("CR", "addressbook-multiget")):
|
_tag("CR", "addressbook-multiget")):
|
||||||
# Read rfc4791-7.9 for info
|
# Read rfc4791-7.9 for info
|
||||||
base_prefix = collection.configuration.get("server", "base_prefix")
|
|
||||||
hreferences = set()
|
hreferences = set()
|
||||||
for href_element in root.findall(_tag("D", "href")):
|
for href_element in root.findall(_tag("D", "href")):
|
||||||
href_path = unquote(urlparse(href_element.text).path)
|
href_path = unquote(urlparse(href_element.text).path)
|
||||||
if href_path.startswith(base_prefix):
|
if (href_path + "/").startswith(base_prefix + "/"):
|
||||||
hreferences.add(href_path[len(base_prefix) - 1:])
|
hreferences.add(href_path[len(base_prefix):])
|
||||||
else:
|
else:
|
||||||
hreferences = (path,)
|
hreferences = (path,)
|
||||||
filters = (
|
filters = (
|
||||||
@ -787,7 +785,8 @@ def report(path, xml_request, collection):
|
|||||||
# Reference is an item
|
# Reference is an item
|
||||||
item = collection.get(name)
|
item = collection.get(name)
|
||||||
if not item:
|
if not item:
|
||||||
response = _item_response(hreference, found_item=False)
|
response = _item_response(base_prefix, hreference,
|
||||||
|
found_item=False)
|
||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
continue
|
continue
|
||||||
items = [item]
|
items = [item]
|
||||||
@ -828,13 +827,14 @@ def report(path, xml_request, collection):
|
|||||||
|
|
||||||
uri = "/" + posixpath.join(collection.path, item.href)
|
uri = "/" + posixpath.join(collection.path, item.href)
|
||||||
multistatus.append(_item_response(
|
multistatus.append(_item_response(
|
||||||
uri, found_props=found_props,
|
base_prefix, uri, found_props=found_props,
|
||||||
not_found_props=not_found_props, found_item=True))
|
not_found_props=not_found_props, found_item=True))
|
||||||
|
|
||||||
return _pretty_xml(multistatus)
|
return _pretty_xml(multistatus)
|
||||||
|
|
||||||
|
|
||||||
def _item_response(href, found_props=(), not_found_props=(), found_item=True):
|
def _item_response(base_prefix, href, found_props=(), not_found_props=(),
|
||||||
|
found_item=True):
|
||||||
response = ET.Element(_tag("D", "response"))
|
response = ET.Element(_tag("D", "response"))
|
||||||
|
|
||||||
href_tag = ET.Element(_tag("D", "href"))
|
href_tag = ET.Element(_tag("D", "href"))
|
||||||
|
Loading…
Reference in New Issue
Block a user