mirror of
https://github.com/Kozea/Radicale.git
synced 2025-06-26 16:45:52 +00:00
Resolved conflicts
This commit is contained in:
parent
cf81d1f9a7
commit
dd723dae5d
3 changed files with 99 additions and 100 deletions
|
@ -1,4 +1,4 @@
|
||||||
# This file is part of Radicale Server - Calendar Server
|
# This file is part of Radicale - CalDAV and CardDAV server
|
||||||
# Copyright © 2008 Nicolas Kandel
|
# Copyright © 2008 Nicolas Kandel
|
||||||
# Copyright © 2008 Pascal Halter
|
# Copyright © 2008 Pascal Halter
|
||||||
# Copyright © 2008-2017 Guillaume Ayoub
|
# Copyright © 2008-2017 Guillaume Ayoub
|
||||||
|
@ -17,28 +17,31 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
from http import client
|
from http import client
|
||||||
from xml.etree import ElementTree as ET
|
from typing import Optional
|
||||||
|
|
||||||
from radicale import app, httputils, storage, xmlutils
|
from radicale import httputils, storage, types, xmlutils
|
||||||
|
from radicale.app.base import Access, ApplicationBase
|
||||||
from radicale.hook import HookNotificationItem, HookNotificationItemTypes
|
from radicale.hook import HookNotificationItem, HookNotificationItemTypes
|
||||||
|
|
||||||
|
|
||||||
def xml_delete(base_prefix, path, collection, href=None):
|
def xml_delete(base_prefix: str, path: str, collection: storage.BaseCollection,
|
||||||
|
item_href: Optional[str] = None) -> ET.Element:
|
||||||
"""Read and answer DELETE requests.
|
"""Read and answer DELETE requests.
|
||||||
|
|
||||||
Read rfc4918-9.6 for info.
|
Read rfc4918-9.6 for info.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
collection.delete(href)
|
collection.delete(item_href)
|
||||||
|
|
||||||
multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
|
multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
|
||||||
response = ET.Element(xmlutils.make_clark("D:response"))
|
response = ET.Element(xmlutils.make_clark("D:response"))
|
||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
|
|
||||||
href = ET.Element(xmlutils.make_clark("D:href"))
|
href_element = ET.Element(xmlutils.make_clark("D:href"))
|
||||||
href.text = xmlutils.make_href(base_prefix, path)
|
href_element.text = xmlutils.make_href(base_prefix, path)
|
||||||
response.append(href)
|
response.append(href_element)
|
||||||
|
|
||||||
status = ET.Element(xmlutils.make_clark("D:status"))
|
status = ET.Element(xmlutils.make_clark("D:status"))
|
||||||
status.text = xmlutils.make_response(200)
|
status.text = xmlutils.make_response(200)
|
||||||
|
@ -47,14 +50,16 @@ def xml_delete(base_prefix, path, collection, href=None):
|
||||||
return multistatus
|
return multistatus
|
||||||
|
|
||||||
|
|
||||||
class ApplicationDeleteMixin:
|
class ApplicationPartDelete(ApplicationBase):
|
||||||
def do_DELETE(self, environ, base_prefix, path, user):
|
|
||||||
|
def do_DELETE(self, environ: types.WSGIEnviron, base_prefix: str,
|
||||||
|
path: str, user: str) -> types.WSGIResponse:
|
||||||
"""Manage DELETE request."""
|
"""Manage DELETE request."""
|
||||||
access = app.Access(self._rights, user, path)
|
access = Access(self._rights, user, path)
|
||||||
if not access.check("w"):
|
if not access.check("w"):
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
with self._storage.acquire_lock("w", user):
|
with self._storage.acquire_lock("w", user):
|
||||||
item = next(self._storage.discover(path), None)
|
item = next(iter(self._storage.discover(path)), None)
|
||||||
if not item:
|
if not item:
|
||||||
return httputils.NOT_FOUND
|
return httputils.NOT_FOUND
|
||||||
if not access.check("w", item):
|
if not access.check("w", item):
|
||||||
|
@ -75,6 +80,8 @@ class ApplicationDeleteMixin:
|
||||||
)
|
)
|
||||||
xml_answer = xml_delete(base_prefix, path, item)
|
xml_answer = xml_delete(base_prefix, path, item)
|
||||||
else:
|
else:
|
||||||
|
assert item.collection is not None
|
||||||
|
assert item.href is not None
|
||||||
hook_notification_item_list.append(
|
hook_notification_item_list.append(
|
||||||
HookNotificationItem(
|
HookNotificationItem(
|
||||||
HookNotificationItemTypes.DELETE,
|
HookNotificationItemTypes.DELETE,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# This file is part of Radicale Server - Calendar Server
|
# This file is part of Radicale - CalDAV and CardDAV server
|
||||||
# Copyright © 2008 Nicolas Kandel
|
# Copyright © 2008 Nicolas Kandel
|
||||||
# Copyright © 2008 Pascal Halter
|
# Copyright © 2008 Pascal Halter
|
||||||
# Copyright © 2008-2017 Guillaume Ayoub
|
# Copyright © 2008-2017 Guillaume Ayoub
|
||||||
|
@ -18,92 +18,71 @@
|
||||||
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
# along with Radicale. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
from http import client
|
from http import client
|
||||||
from xml.etree import ElementTree as ET
|
from typing import Dict, Optional, cast
|
||||||
|
|
||||||
import defusedxml.ElementTree as DefusedET
|
import radicale.item as radicale_item
|
||||||
|
from radicale import httputils, storage, types, xmlutils
|
||||||
from radicale import app, httputils
|
from radicale.app.base import Access, ApplicationBase
|
||||||
from radicale import item as radicale_item
|
|
||||||
from radicale import storage, xmlutils
|
|
||||||
from radicale.hook import HookNotificationItem, HookNotificationItemTypes
|
from radicale.hook import HookNotificationItem, HookNotificationItemTypes
|
||||||
from radicale.log import logger
|
from radicale.log import logger
|
||||||
|
|
||||||
|
|
||||||
def xml_add_propstat_to(element, tag, status_number):
|
def xml_proppatch(base_prefix: str, path: str,
|
||||||
"""Add a PROPSTAT response structure to an element.
|
xml_request: Optional[ET.Element],
|
||||||
|
collection: storage.BaseCollection) -> ET.Element:
|
||||||
The PROPSTAT answer structure is defined in rfc4918-9.1. It is added to the
|
|
||||||
given ``element``, for the following ``tag`` with the given
|
|
||||||
``status_number``.
|
|
||||||
|
|
||||||
"""
|
|
||||||
propstat = ET.Element(xmlutils.make_clark("D:propstat"))
|
|
||||||
element.append(propstat)
|
|
||||||
|
|
||||||
prop = ET.Element(xmlutils.make_clark("D:prop"))
|
|
||||||
propstat.append(prop)
|
|
||||||
|
|
||||||
clark_tag = xmlutils.make_clark(tag)
|
|
||||||
prop_tag = ET.Element(clark_tag)
|
|
||||||
prop.append(prop_tag)
|
|
||||||
|
|
||||||
status = ET.Element(xmlutils.make_clark("D:status"))
|
|
||||||
status.text = xmlutils.make_response(status_number)
|
|
||||||
propstat.append(status)
|
|
||||||
|
|
||||||
|
|
||||||
def xml_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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
props_to_set = xmlutils.props_from_request(xml_request, actions=("set",))
|
|
||||||
props_to_remove = xmlutils.props_from_request(xml_request,
|
|
||||||
actions=("remove",))
|
|
||||||
|
|
||||||
multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
|
multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
|
||||||
response = ET.Element(xmlutils.make_clark("D:response"))
|
response = ET.Element(xmlutils.make_clark("D:response"))
|
||||||
multistatus.append(response)
|
multistatus.append(response)
|
||||||
|
|
||||||
href = ET.Element(xmlutils.make_clark("D:href"))
|
href = ET.Element(xmlutils.make_clark("D:href"))
|
||||||
href.text = xmlutils.make_href(base_prefix, path)
|
href.text = xmlutils.make_href(base_prefix, path)
|
||||||
response.append(href)
|
response.append(href)
|
||||||
|
# Create D:propstat element for props with status 200 OK
|
||||||
|
propstat = ET.Element(xmlutils.make_clark("D:propstat"))
|
||||||
|
status = ET.Element(xmlutils.make_clark("D:status"))
|
||||||
|
status.text = xmlutils.make_response(200)
|
||||||
|
props_ok = ET.Element(xmlutils.make_clark("D:prop"))
|
||||||
|
propstat.append(props_ok)
|
||||||
|
propstat.append(status)
|
||||||
|
response.append(propstat)
|
||||||
|
|
||||||
new_props = collection.get_meta()
|
props_with_remove = xmlutils.props_from_request(xml_request)
|
||||||
for short_name, value in props_to_set.items():
|
all_props_with_remove = cast(Dict[str, Optional[str]],
|
||||||
new_props[short_name] = value
|
dict(collection.get_meta()))
|
||||||
xml_add_propstat_to(response, short_name, 200)
|
all_props_with_remove.update(props_with_remove)
|
||||||
for short_name in props_to_remove:
|
all_props = radicale_item.check_and_sanitize_props(all_props_with_remove)
|
||||||
try:
|
collection.set_meta(all_props)
|
||||||
del new_props[short_name]
|
for short_name in props_with_remove:
|
||||||
except KeyError:
|
props_ok.append(ET.Element(xmlutils.make_clark(short_name)))
|
||||||
pass
|
|
||||||
xml_add_propstat_to(response, short_name, 200)
|
|
||||||
radicale_item.check_and_sanitize_props(new_props)
|
|
||||||
collection.set_meta(new_props)
|
|
||||||
|
|
||||||
return multistatus
|
return multistatus
|
||||||
|
|
||||||
|
|
||||||
class ApplicationProppatchMixin:
|
class ApplicationPartProppatch(ApplicationBase):
|
||||||
def do_PROPPATCH(self, environ, base_prefix, path, user):
|
|
||||||
|
def do_PROPPATCH(self, environ: types.WSGIEnviron, base_prefix: str,
|
||||||
|
path: str, user: str) -> types.WSGIResponse:
|
||||||
"""Manage PROPPATCH request."""
|
"""Manage PROPPATCH request."""
|
||||||
access = app.Access(self._rights, user, path)
|
access = Access(self._rights, user, path)
|
||||||
if not access.check("w"):
|
if not access.check("w"):
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
try:
|
try:
|
||||||
xml_content = self._read_xml_content(environ)
|
xml_content = self._read_xml_request_body(environ)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Bad PROPPATCH request on %r: %s", path, e, exc_info=True)
|
"Bad PROPPATCH request on %r: %s", path, e, exc_info=True)
|
||||||
return httputils.BAD_REQUEST
|
return httputils.BAD_REQUEST
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
logger.debug("client timed out", exc_info=True)
|
logger.debug("Client timed out", exc_info=True)
|
||||||
return httputils.REQUEST_TIMEOUT
|
return httputils.REQUEST_TIMEOUT
|
||||||
with self._storage.acquire_lock("w", user):
|
with self._storage.acquire_lock("w", user):
|
||||||
item = next(self._storage.discover(path), None)
|
item = next(iter(self._storage.discover(path)), None)
|
||||||
if not item:
|
if not item:
|
||||||
return httputils.NOT_FOUND
|
return httputils.NOT_FOUND
|
||||||
if not access.check("w", item):
|
if not access.check("w", item):
|
||||||
|
@ -129,5 +108,4 @@ class ApplicationProppatchMixin:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Bad PROPPATCH request on %r: %s", path, e, exc_info=True)
|
"Bad PROPPATCH request on %r: %s", path, e, exc_info=True)
|
||||||
return httputils.BAD_REQUEST
|
return httputils.BAD_REQUEST
|
||||||
return (client.MULTI_STATUS, headers,
|
return client.MULTI_STATUS, headers, self._xml_response(xml_answer)
|
||||||
self._write_xml_content(xml_answer))
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# This file is part of Radicale Server - Calendar Server
|
# This file is part of Radicale - CalDAV and CardDAV server
|
||||||
# Copyright © 2008 Nicolas Kandel
|
# Copyright © 2008 Nicolas Kandel
|
||||||
# Copyright © 2008 Pascal Halter
|
# Copyright © 2008 Pascal Halter
|
||||||
# Copyright © 2008-2017 Guillaume Ayoub
|
# Copyright © 2008-2017 Guillaume Ayoub
|
||||||
|
@ -22,21 +22,31 @@ import posixpath
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
from http import client
|
from http import client
|
||||||
|
from typing import Iterator, List, Mapping, MutableMapping, Optional, Tuple
|
||||||
|
|
||||||
import vobject
|
import vobject
|
||||||
|
|
||||||
from radicale import app, httputils
|
import radicale.item as radicale_item
|
||||||
from radicale import item as radicale_item
|
from radicale import httputils, pathutils, rights, storage, types, xmlutils
|
||||||
from radicale import pathutils, rights, storage, xmlutils
|
from radicale.app.base import Access, ApplicationBase
|
||||||
from radicale.hook import HookNotificationItem, HookNotificationItemTypes
|
from radicale.hook import HookNotificationItem, HookNotificationItemTypes
|
||||||
from radicale.log import logger
|
from radicale.log import logger
|
||||||
|
from types import TracebackType
|
||||||
|
|
||||||
MIMETYPE_TAGS = {value: key for key, value in xmlutils.MIMETYPES.items()}
|
MIMETYPE_TAGS: Mapping[str, str] = {value: key for key, value in
|
||||||
|
xmlutils.MIMETYPES.items()}
|
||||||
|
|
||||||
|
|
||||||
def prepare(vobject_items, path, content_type, permissions, parent_permissions,
|
def prepare(vobject_items: List[vobject.base.Component], path: str,
|
||||||
tag=None, write_whole_collection=None):
|
content_type: str, permission: bool, parent_permission: bool,
|
||||||
if (write_whole_collection or permissions and not parent_permissions):
|
tag: Optional[str] = None,
|
||||||
|
write_whole_collection: Optional[bool] = None) -> Tuple[
|
||||||
|
Iterator[radicale_item.Item], # items
|
||||||
|
Optional[str], # tag
|
||||||
|
Optional[bool], # write_whole_collection
|
||||||
|
Optional[MutableMapping[str, str]], # props
|
||||||
|
Optional[Tuple[type, BaseException, Optional[TracebackType]]]]:
|
||||||
|
if (write_whole_collection or permission and not parent_permission):
|
||||||
write_whole_collection = True
|
write_whole_collection = True
|
||||||
tag = radicale_item.predict_tag_of_whole_collection(
|
tag = radicale_item.predict_tag_of_whole_collection(
|
||||||
vobject_items, MIMETYPE_TAGS.get(content_type))
|
vobject_items, MIMETYPE_TAGS.get(content_type))
|
||||||
|
@ -44,20 +54,20 @@ def prepare(vobject_items, path, content_type, permissions, parent_permissions,
|
||||||
raise ValueError("Can't determine collection tag")
|
raise ValueError("Can't determine collection tag")
|
||||||
collection_path = pathutils.strip_path(path)
|
collection_path = pathutils.strip_path(path)
|
||||||
elif (write_whole_collection is not None and not write_whole_collection or
|
elif (write_whole_collection is not None and not write_whole_collection or
|
||||||
not permissions and parent_permissions):
|
not permission and parent_permission):
|
||||||
write_whole_collection = False
|
write_whole_collection = False
|
||||||
if tag is None:
|
if tag is None:
|
||||||
tag = radicale_item.predict_tag_of_parent_collection(vobject_items)
|
tag = radicale_item.predict_tag_of_parent_collection(vobject_items)
|
||||||
collection_path = posixpath.dirname(pathutils.strip_path(path))
|
collection_path = posixpath.dirname(pathutils.strip_path(path))
|
||||||
props = None
|
props: Optional[MutableMapping[str, str]] = None
|
||||||
stored_exc_info = None
|
stored_exc_info = None
|
||||||
items = []
|
items = []
|
||||||
try:
|
try:
|
||||||
if tag:
|
if tag and write_whole_collection is not None:
|
||||||
radicale_item.check_and_sanitize_items(
|
radicale_item.check_and_sanitize_items(
|
||||||
vobject_items, is_collection=write_whole_collection, tag=tag)
|
vobject_items, is_collection=write_whole_collection, tag=tag)
|
||||||
if write_whole_collection and tag == "VCALENDAR":
|
if write_whole_collection and tag == "VCALENDAR":
|
||||||
vobject_components = []
|
vobject_components: List[vobject.base.Component] = []
|
||||||
vobject_item, = vobject_items
|
vobject_item, = vobject_items
|
||||||
for content in ("vevent", "vtodo", "vjournal"):
|
for content in ("vevent", "vtodo", "vjournal"):
|
||||||
vobject_components.extend(
|
vobject_components.extend(
|
||||||
|
@ -99,37 +109,40 @@ def prepare(vobject_items, path, content_type, permissions, parent_permissions,
|
||||||
caldesc = vobject_items[0].x_wr_caldesc.value
|
caldesc = vobject_items[0].x_wr_caldesc.value
|
||||||
if caldesc:
|
if caldesc:
|
||||||
props["C:calendar-description"] = caldesc
|
props["C:calendar-description"] = caldesc
|
||||||
radicale_item.check_and_sanitize_props(props)
|
props = radicale_item.check_and_sanitize_props(props)
|
||||||
except Exception:
|
except Exception:
|
||||||
stored_exc_info = sys.exc_info()
|
exc_info_or_none_tuple = sys.exc_info()
|
||||||
|
assert exc_info_or_none_tuple[0] is not None
|
||||||
|
stored_exc_info = exc_info_or_none_tuple
|
||||||
|
|
||||||
# Use generator for items and delete references to free memory
|
# Use iterator for items and delete references to free memory early
|
||||||
# early
|
def items_iter() -> Iterator[radicale_item.Item]:
|
||||||
def items_generator():
|
|
||||||
while items:
|
while items:
|
||||||
yield items.pop(0)
|
yield items.pop(0)
|
||||||
return (items_generator(), tag, write_whole_collection, props,
|
return items_iter(), tag, write_whole_collection, props, stored_exc_info
|
||||||
stored_exc_info)
|
|
||||||
|
|
||||||
|
|
||||||
class ApplicationPutMixin:
|
class ApplicationPartPut(ApplicationBase):
|
||||||
def do_PUT(self, environ, base_prefix, path, user):
|
|
||||||
|
def do_PUT(self, environ: types.WSGIEnviron, base_prefix: str,
|
||||||
|
path: str, user: str) -> types.WSGIResponse:
|
||||||
"""Manage PUT request."""
|
"""Manage PUT request."""
|
||||||
access = app.Access(self._rights, user, path)
|
access = Access(self._rights, user, path)
|
||||||
if not access.check("w"):
|
if not access.check("w"):
|
||||||
return httputils.NOT_ALLOWED
|
return httputils.NOT_ALLOWED
|
||||||
try:
|
try:
|
||||||
content = self._read_content(environ)
|
content = httputils.read_request_body(self.configuration, environ)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True)
|
logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True)
|
||||||
return httputils.BAD_REQUEST
|
return httputils.BAD_REQUEST
|
||||||
except socket.timeout:
|
except socket.timeout:
|
||||||
logger.debug("client timed out", exc_info=True)
|
logger.debug("Client timed out", exc_info=True)
|
||||||
return httputils.REQUEST_TIMEOUT
|
return httputils.REQUEST_TIMEOUT
|
||||||
# Prepare before locking
|
# Prepare before locking
|
||||||
content_type = environ.get("CONTENT_TYPE", "").split(";")[0]
|
content_type = environ.get("CONTENT_TYPE", "").split(";",
|
||||||
|
maxsplit=1)[0]
|
||||||
try:
|
try:
|
||||||
vobject_items = tuple(vobject.readComponents(content or ""))
|
vobject_items = radicale_item.read_components(content or "")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Bad PUT request on %r: %s", path, e, exc_info=True)
|
"Bad PUT request on %r: %s", path, e, exc_info=True)
|
||||||
|
@ -141,20 +154,20 @@ class ApplicationPutMixin:
|
||||||
bool(rights.intersect(access.parent_permissions, "w")))
|
bool(rights.intersect(access.parent_permissions, "w")))
|
||||||
|
|
||||||
with self._storage.acquire_lock("w", user):
|
with self._storage.acquire_lock("w", user):
|
||||||
item = next(self._storage.discover(path), None)
|
item = next(iter(self._storage.discover(path)), None)
|
||||||
parent_item = next(
|
parent_item = next(iter(
|
||||||
self._storage.discover(access.parent_path), None)
|
self._storage.discover(access.parent_path)), None)
|
||||||
if not parent_item:
|
if not isinstance(parent_item, storage.BaseCollection):
|
||||||
return httputils.CONFLICT
|
return httputils.CONFLICT
|
||||||
|
|
||||||
write_whole_collection = (
|
write_whole_collection = (
|
||||||
isinstance(item, storage.BaseCollection) or
|
isinstance(item, storage.BaseCollection) or
|
||||||
not parent_item.get_meta("tag"))
|
not parent_item.tag)
|
||||||
|
|
||||||
if write_whole_collection:
|
if write_whole_collection:
|
||||||
tag = prepared_tag
|
tag = prepared_tag
|
||||||
else:
|
else:
|
||||||
tag = parent_item.get_meta("tag")
|
tag = parent_item.tag
|
||||||
|
|
||||||
if write_whole_collection:
|
if write_whole_collection:
|
||||||
if ("w" if tag else "W") not in access.permissions:
|
if ("w" if tag else "W") not in access.permissions:
|
||||||
|
@ -206,6 +219,7 @@ class ApplicationPutMixin:
|
||||||
"Bad PUT request on %r: %s", path, e, exc_info=True)
|
"Bad PUT request on %r: %s", path, e, exc_info=True)
|
||||||
return httputils.BAD_REQUEST
|
return httputils.BAD_REQUEST
|
||||||
else:
|
else:
|
||||||
|
assert not isinstance(item, storage.BaseCollection)
|
||||||
prepared_item, = prepared_items
|
prepared_item, = prepared_items
|
||||||
if (item and item.uid != prepared_item.uid or
|
if (item and item.uid != prepared_item.uid or
|
||||||
not item and parent_item.has_uid(prepared_item.uid)):
|
not item and parent_item.has_uid(prepared_item.uid)):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue