def discover(cls, path, depth="0", child_context_manager=( lambda path, href=None: contextlib.ExitStack())): # Path should already be sanitized sane_path = pathutils.strip_path(path) attributes = sane_path.split("/") if sane_path else [] folder = cls._get_collection_root_folder() # Create the root collection cls._makedirs_synced(folder) try: filesystem_path = pathutils.path_to_filesystem(folder, sane_path) except ValueError as e: # Path is unsafe logger.debug("Unsafe path %r requested from storage: %s", sane_path, e, exc_info=True) return # Check if the path exists and if it leads to a collection or an item if not os.path.isdir(filesystem_path): if attributes and os.path.isfile(filesystem_path): href = attributes.pop() else: return else: href = None sane_path = "/".join(attributes) collection = cls(pathutils.unstrip_path(sane_path, True)) if href: yield collection._get(href) return yield collection if depth == "0": return for href in collection._list(): with child_context_manager(sane_path, href): yield collection._get(href) for entry in os.scandir(filesystem_path): if not entry.is_dir(): continue href = entry.name if not pathutils.is_safe_filesystem_path_component(href): if not href.startswith(".Radicale"): logger.debug("Skipping collection %r in %r", href, sane_path) continue sane_child_path = posixpath.join(sane_path, href) child_path = pathutils.unstrip_path(sane_child_path, True) with child_context_manager(sane_child_path): yield cls(child_path)
def _collect_allowed_items(self, items, user): """Get items from request that user is allowed to access.""" for item in items: if isinstance(item, storage.BaseCollection): path = pathutils.unstrip_path(item.path, True) if item.get_meta("tag"): permissions = self.Rights.authorized(user, path, "rw") target = "collection with tag %r" % item.path else: permissions = self.Rights.authorized(user, path, "RW") target = "collection %r" % item.path else: path = pathutils.unstrip_path(item.collection.path, True) permissions = self.Rights.authorized(user, path, "rw") target = "item %r from %r" % (item.href, item.collection.path) if rights.intersect_permissions(permissions, "Ww"): permission = "w" status = "write" elif rights.intersect_permissions(permissions, "Rr"): permission = "r" status = "read" else: permission = "" status = "NO" logger.debug( "%s has %s access to %s", repr(user) if user else "anonymous user", status, target) if permission: yield item, permission
def _collect_allowed_items(self, items, user): """Get items from request that user is allowed to access.""" for item in items: if isinstance(item, storage.BaseCollection): path = pathutils.unstrip_path(item.path, True) if item.get_meta("tag"): permissions = rights.intersect( self._rights.authorization(user, path), "rw") target = "collection with tag %r" % item.path else: permissions = rights.intersect( self._rights.authorization(user, path), "RW") target = "collection %r" % item.path else: path = pathutils.unstrip_path(item.collection.path, True) permissions = rights.intersect( self._rights.authorization(user, path), "rw") target = "item %r from %r" % (item.href, item.collection.path) if rights.intersect(permissions, "Ww"): permission = "w" status = "write" elif rights.intersect(permissions, "Rr"): permission = "r" status = "read" else: permission = "" status = "NO" logger.debug("%s has %s access to %s", repr(user) if user else "anonymous user", status, target) if permission: yield item, permission
def _access(self, user, path, permission, item=None): if permission not in "rw": raise ValueError("Invalid permission argument: %r" % permission) if not item: permissions = permission + permission.upper() parent_permissions = permission elif isinstance(item, storage.BaseCollection): if item.get_meta("tag"): permissions = permission else: permissions = permission.upper() parent_permissions = "" else: permissions = "" parent_permissions = permission if permissions and rights.intersect( self._rights.authorization(user, path), permissions): return True if parent_permissions: parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) if rights.intersect(self._rights.authorization(user, parent_path), parent_permissions): return True return False
def do_MOVE(self, environ, base_prefix, path, user): """Manage MOVE request.""" raw_dest = environ.get("HTTP_DESTINATION", "") to_url = urlparse(raw_dest) if to_url.netloc != environ["HTTP_HOST"]: logger.info("Unsupported destination address: %r", raw_dest) # Remote destination server, not supported return httputils.REMOTE_DESTINATION if not self.access(user, path, "w"): return httputils.NOT_ALLOWED to_path = pathutils.sanitize_path(to_url.path) if not (to_path + "/").startswith(base_prefix + "/"): logger.warning( "Destination %r from MOVE request on %r doesn't " "start with base prefix", to_path, path) return httputils.NOT_ALLOWED to_path = to_path[len(base_prefix):] if not self.access(user, to_path, "w"): return httputils.NOT_ALLOWED with self.Collection.acquire_lock("w", user): item = next(self.Collection.discover(path), None) if not item: return httputils.NOT_FOUND if (not self.access(user, path, "w", item) or not self.access(user, to_path, "w", item)): return httputils.NOT_ALLOWED if isinstance(item, storage.BaseCollection): # TODO: support moving collections return httputils.METHOD_NOT_ALLOWED to_item = next(self.Collection.discover(to_path), None) if isinstance(to_item, storage.BaseCollection): return httputils.FORBIDDEN to_parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(to_path)), True) to_collection = next(self.Collection.discover(to_parent_path), None) if not to_collection: return httputils.CONFLICT tag = item.collection.get_meta("tag") if not tag or tag != to_collection.get_meta("tag"): return httputils.FORBIDDEN if to_item and environ.get("HTTP_OVERWRITE", "F") != "T": return httputils.PRECONDITION_FAILED if (to_item and item.uid != to_item.uid or not to_item and to_collection.path != item.collection.path and to_collection.has_uid(item.uid)): return self.webdav_error_response( "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict") to_href = posixpath.basename(pathutils.strip_path(to_path)) try: self.Collection.move(item, to_collection, to_href) except ValueError as e: logger.warning("Bad MOVE request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.NO_CONTENT if to_item else client.CREATED, {}, None
def __init__(self, rights, user, path): self._rights = rights self.user = user self.path = path self.parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) self.permissions = self._rights.authorization(self.user, self.path) self._parent_permissions = None
def do_MOVE(self, environ, base_prefix, path, user): """Manage MOVE request.""" raw_dest = environ.get("HTTP_DESTINATION", "") to_url = urlparse(raw_dest) if to_url.netloc != environ["HTTP_HOST"]: logger.info("Unsupported destination address: %r", raw_dest) # Remote destination server, not supported return httputils.REMOTE_DESTINATION if not self.access(user, path, "w"): return httputils.NOT_ALLOWED to_path = pathutils.sanitize_path(to_url.path) if not (to_path + "/").startswith(base_prefix + "/"): logger.warning("Destination %r from MOVE request on %r doesn't " "start with base prefix", to_path, path) return httputils.NOT_ALLOWED to_path = to_path[len(base_prefix):] if not self.access(user, to_path, "w"): return httputils.NOT_ALLOWED with self.Collection.acquire_lock("w", user): item = next(self.Collection.discover(path), None) if not item: return httputils.NOT_FOUND if (not self.access(user, path, "w", item) or not self.access(user, to_path, "w", item)): return httputils.NOT_ALLOWED if isinstance(item, storage.BaseCollection): # TODO: support moving collections return httputils.METHOD_NOT_ALLOWED to_item = next(self.Collection.discover(to_path), None) if isinstance(to_item, storage.BaseCollection): return httputils.FORBIDDEN to_parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(to_path)), True) to_collection = next( self.Collection.discover(to_parent_path), None) if not to_collection: return httputils.CONFLICT tag = item.collection.get_meta("tag") if not tag or tag != to_collection.get_meta("tag"): return httputils.FORBIDDEN if to_item and environ.get("HTTP_OVERWRITE", "F") != "T": return httputils.PRECONDITION_FAILED if (to_item and item.uid != to_item.uid or not to_item and to_collection.path != item.collection.path and to_collection.has_uid(item.uid)): return self.webdav_error_response( "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict") to_href = posixpath.basename(pathutils.strip_path(to_path)) try: self.Collection.move(item, to_collection, to_href) except ValueError as e: logger.warning( "Bad MOVE request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.NO_CONTENT if to_item else client.CREATED, {}, None
def create_collection(self, href, items=None, props=None): folder = self._get_collection_root_folder() # Path should already be sanitized sane_path = pathutils.strip_path(href) filesystem_path = pathutils.path_to_filesystem(folder, sane_path) if not props: self._makedirs_synced(filesystem_path) return self._collection_class( self, pathutils.unstrip_path(sane_path, True)) parent_dir = os.path.dirname(filesystem_path) self._makedirs_synced(parent_dir) # Create a temporary directory with an unsafe name with TemporaryDirectory(prefix=".Radicale.tmp-", dir=parent_dir) as tmp_dir: # The temporary directory itself can't be renamed tmp_filesystem_path = os.path.join(tmp_dir, "collection") os.makedirs(tmp_filesystem_path) col = self._collection_class(self, pathutils.unstrip_path( sane_path, True), filesystem_path=tmp_filesystem_path) col.set_meta(props) if items is not None: if props.get("tag") == "VCALENDAR": col._upload_all_nonatomic(items, suffix=".ics") elif props.get("tag") == "VADDRESSBOOK": col._upload_all_nonatomic(items, suffix=".vcf") # This operation is not atomic on the filesystem level but it's # very unlikely that one rename operations succeeds while the # other fails or that only one gets written to disk. if os.path.exists(filesystem_path): os.rename(filesystem_path, os.path.join(tmp_dir, "delete")) os.rename(tmp_filesystem_path, filesystem_path) self._sync_directory(parent_dir) return self._collection_class(self, pathutils.unstrip_path(sane_path, True))
def verify(cls): item_errors = collection_errors = 0 @contextlib.contextmanager def exception_cm(path, href=None): nonlocal item_errors, collection_errors sane_path = pathutils.strip_path(path) try: yield except Exception as e: if href: item_errors += 1 name = "item %r in %r" % (href, sane_path) else: collection_errors += 1 name = "collection %r" % sane_path logger.error("Invalid %s: %s", name, e, exc_info=True) remaining_sane_paths = [""] while remaining_sane_paths: sane_path = remaining_sane_paths.pop(0) path = pathutils.unstrip_path(sane_path, True) logger.debug("Verifying collection %r", sane_path) with exception_cm(path): saved_item_errors = item_errors collection = None uids = set() has_child_collections = False for item in cls.discover(path, "1", exception_cm): if not collection: collection = item collection.get_meta() continue if isinstance(item, storage.BaseCollection): has_child_collections = True remaining_sane_paths.append(item.path) elif item.uid in uids: cls.logger.error( "Invalid item %r in %r: UID conflict %r", item.href, sane_path, item.uid) else: uids.add(item.uid) logger.debug("Verified item %r in %r", item.href, sane_path) if item_errors == saved_item_errors: collection.sync() if has_child_collections and collection.get_meta("tag"): cls.logger.error( "Invalid collection %r: %r must not have " "child collections", sane_path, collection.get_meta("tag")) return item_errors == 0 and collection_errors == 0
def create_collection(self, href, items=None, props=None): folder = self._get_collection_root_folder() # Path should already be sanitized sane_path = pathutils.strip_path(href) filesystem_path = pathutils.path_to_filesystem(folder, sane_path) if not props: self._makedirs_synced(filesystem_path) return self._collection_class( self, pathutils.unstrip_path(sane_path, True)) parent_dir = os.path.dirname(filesystem_path) self._makedirs_synced(parent_dir) # Create a temporary directory with an unsafe name with TemporaryDirectory( prefix=".Radicale.tmp-", dir=parent_dir) as tmp_dir: # The temporary directory itself can't be renamed tmp_filesystem_path = os.path.join(tmp_dir, "collection") os.makedirs(tmp_filesystem_path) col = self._collection_class( self, pathutils.unstrip_path(sane_path, True), filesystem_path=tmp_filesystem_path) col.set_meta(props) if items is not None: if props.get("tag") == "VCALENDAR": col._upload_all_nonatomic(items, suffix=".ics") elif props.get("tag") == "VADDRESSBOOK": col._upload_all_nonatomic(items, suffix=".vcf") if os.path.lexists(filesystem_path): pathutils.rename_exchange(tmp_filesystem_path, filesystem_path) else: os.rename(tmp_filesystem_path, filesystem_path) self._sync_directory(parent_dir) return self._collection_class( self, pathutils.unstrip_path(sane_path, True))
def do_MKCOL(self, environ, base_prefix, path, user, context=None): """Manage MKCOL request.""" permissions = self._rights.authorization(user, path) if not rights.intersect(permissions, "Ww"): return httputils.NOT_ALLOWED try: xml_content = self._read_xml_request_body(environ) except RuntimeError as e: logger.warning("Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("Client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking props = xmlutils.props_from_request(xml_content) props = {k: v for k, v in props.items() if v is not None} try: radicale_item.check_and_sanitize_props(props) except ValueError as e: logger.warning("Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST if (props.get("tag") and "w" not in permissions or not props.get("tag") and "W" not in permissions): return httputils.NOT_ALLOWED with self._storage.acquire_lock("w", user): item = next(self._storage.discover(path), None) if item: return httputils.METHOD_NOT_ALLOWED parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) parent_item = next(self._storage.discover(parent_path), None) if not parent_item: return httputils.CONFLICT if (not isinstance(parent_item, storage.BaseCollection) or parent_item.get_meta("tag")): return httputils.FORBIDDEN try: self._storage.create_collection(path, props=props) except ValueError as e: logger.warning("Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.CREATED, {}, None
def do_MKCALENDAR(self, environ, base_prefix, path, user): """Manage MKCALENDAR request.""" if "w" not in self._rights.authorization(user, path): return httputils.NOT_ALLOWED try: xml_content = self._read_xml_request_body(environ) except RuntimeError as e: logger.warning("Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("Client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking props = xmlutils.props_from_request(xml_content) props = {k: v for k, v in props.items() if v is not None} props["tag"] = "VCALENDAR" # TODO: use this? # timezone = props.get("C:calendar-timezone") try: radicale_item.check_and_sanitize_props(props) except ValueError as e: logger.warning("Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST with self._storage.acquire_lock("w", user): item = next(self._storage.discover(path), None) if item: return self._webdav_error_response(client.CONFLICT, "D:resource-must-be-null") parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) parent_item = next(self._storage.discover(parent_path), None) if not parent_item: return httputils.CONFLICT if (not isinstance(parent_item, storage.BaseCollection) or parent_item.get_meta("tag")): return httputils.FORBIDDEN try: self._storage.create_collection(path, props=props) except ValueError as e: logger.warning("Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.CREATED, {}, None
def verify(cls): item_errors = collection_errors = 0 @contextlib.contextmanager def exception_cm(sane_path, href=None): nonlocal item_errors, collection_errors try: yield except Exception as e: if href: item_errors += 1 name = "item %r in %r" % (href, sane_path) else: collection_errors += 1 name = "collection %r" % sane_path logger.error("Invalid %s: %s", name, e, exc_info=True) remaining_sane_paths = [""] while remaining_sane_paths: sane_path = remaining_sane_paths.pop(0) path = pathutils.unstrip_path(sane_path, True) logger.debug("Verifying collection %r", sane_path) with exception_cm(sane_path): saved_item_errors = item_errors collection = None uids = set() has_child_collections = False for item in cls.discover(path, "1", exception_cm): if not collection: collection = item collection.get_meta() continue if isinstance(item, storage.BaseCollection): has_child_collections = True remaining_sane_paths.append(item.path) elif item.uid in uids: logger.error("Invalid item %r in %r: UID conflict %r", item.href, sane_path, item.uid) else: uids.add(item.uid) logger.debug("Verified item %r in %r", item.href, sane_path) if item_errors == saved_item_errors: collection.sync() if has_child_collections and collection.get_meta("tag"): logger.error("Invalid collection %r: %r must not have " "child collections", sane_path, collection.get_meta("tag")) return item_errors == 0 and collection_errors == 0
def do_MKCOL(self, environ, base_prefix, path, user): """Manage MKCOL request.""" permissions = self.Rights.authorized(user, path, "Ww") if not permissions: return httputils.NOT_ALLOWED try: xml_content = self.read_xml_content(environ) except RuntimeError as e: logger.warning( "Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking props = xmlutils.props_from_request(xml_content) try: radicale_item.check_and_sanitize_props(props) except ValueError as e: logger.warning( "Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST if (props.get("tag") and "w" not in permissions or not props.get("tag") and "W" not in permissions): return httputils.NOT_ALLOWED with self.Collection.acquire_lock("w", user): item = next(self.Collection.discover(path), None) if item: return httputils.METHOD_NOT_ALLOWED parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) parent_item = next(self.Collection.discover(parent_path), None) if not parent_item: return httputils.CONFLICT if (not isinstance(parent_item, storage.BaseCollection) or parent_item.get_meta("tag")): return httputils.FORBIDDEN try: self.Collection.create_collection(path, props=props) except ValueError as e: logger.warning( "Bad MKCOL request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.CREATED, {}, None
def retrieve_items(collection, hreferences, multistatus): """Retrieves all items that are referenced in ``hreferences`` from ``collection`` and adds 404 responses for missing and invalid items to ``multistatus``.""" collection_requested = False def get_names(): """Extracts all names from references in ``hreferences`` and adds 404 responses for invalid references to ``multistatus``. If the whole collections is referenced ``collection_requested`` gets set to ``True``.""" nonlocal collection_requested for hreference in hreferences: try: name = pathutils.name_from_path(hreference, collection) except ValueError as e: logger.warning( "Skipping invalid path %r in REPORT request" " on %r: %s", hreference, path, e) response = xml_item_response(base_prefix, hreference, found_item=False) multistatus.append(response) continue if name: # Reference is an item yield name else: # Reference is a collection collection_requested = True for name, item in collection.get_multi(get_names()): if not item: uri = pathutils.unstrip_path( posixpath.join(collection.path, name)) response = xml_item_response(base_prefix, uri, found_item=False) multistatus.append(response) else: yield item, False if collection_requested: yield from collection.get_filtered(filters)
def do_MKCALENDAR(self, environ, base_prefix, path, user): """Manage MKCALENDAR request.""" if not self.Rights.authorized(user, path, "w"): return httputils.NOT_ALLOWED try: xml_content = self.read_xml_content(environ) except RuntimeError as e: logger.warning( "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking props = xmlutils.props_from_request(xml_content) props["tag"] = "VCALENDAR" # TODO: use this? # timezone = props.get("C:calendar-timezone") try: radicale_item.check_and_sanitize_props(props) except ValueError as e: logger.warning( "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) with self.Collection.acquire_lock("w", user): item = next(self.Collection.discover(path), None) if item: return self.webdav_error_response( "D", "resource-must-be-null") parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) parent_item = next(self.Collection.discover(parent_path), None) if not parent_item: return httputils.CONFLICT if (not isinstance(parent_item, storage.BaseCollection) or parent_item.get_meta("tag")): return httputils.FORBIDDEN try: self.Collection.create_collection(path, props=props) except ValueError as e: logger.warning( "Bad MKCALENDAR request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST return client.CREATED, {}, None
def retrieve_items(collection, hreferences, multistatus): """Retrieves all items that are referenced in ``hreferences`` from ``collection`` and adds 404 responses for missing and invalid items to ``multistatus``.""" collection_requested = False def get_names(): """Extracts all names from references in ``hreferences`` and adds 404 responses for invalid references to ``multistatus``. If the whole collections is referenced ``collection_requested`` gets set to ``True``.""" nonlocal collection_requested for hreference in hreferences: try: name = pathutils.name_from_path(hreference, collection) except ValueError as e: logger.warning("Skipping invalid path %r in REPORT request" " on %r: %s", hreference, path, e) response = xml_item_response(base_prefix, hreference, found_item=False) multistatus.append(response) continue if name: # Reference is an item yield name else: # Reference is a collection collection_requested = True for name, item in collection.get_multi(get_names()): if not item: uri = pathutils.unstrip_path( posixpath.join(collection.path, name)) response = xml_item_response(base_prefix, uri, found_item=False) multistatus.append(response) else: yield item, False if collection_requested: yield from collection.get_filtered(filters)
def access(self, user, path, permission, item=None): if permission not in "rw": raise ValueError("Invalid permission argument: %r" % permission) if not item: permissions = permission + permission.upper() parent_permissions = permission elif isinstance(item, storage.BaseCollection): if item.get_meta("tag"): permissions = permission else: permissions = permission.upper() parent_permissions = "" else: permissions = "" parent_permissions = permission if permissions and self.Rights.authorized(user, path, permissions): return True if parent_permissions: parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) if self.Rights.authorized(user, parent_path, parent_permissions): return True return False
def xml_report(base_prefix, path, xml_request, collection, encoding, unlock_storage_fn): """Read and answer REPORT requests. Read rfc3253-3.6 for info. """ multistatus = ET.Element(xmlutils.make_clark("D:multistatus")) if xml_request is None: return client.MULTI_STATUS, multistatus root = xml_request if root.tag in (xmlutils.make_clark("D:principal-search-property-set"), xmlutils.make_clark("D:principal-property-search"), xmlutils.make_clark("D:expand-property")): # We don't support searching for principals or indirect retrieving of # properties, just return an empty result. # InfCloud asks for expand-property reports (even if we don't announce # support for them) and stops working if an error code is returned. logger.warning("Unsupported REPORT method %r on %r requested", xmlutils.make_human_tag(root.tag), path) return client.MULTI_STATUS, multistatus if (root.tag == xmlutils.make_clark("C:calendar-multiget") and collection.get_meta("tag") != "VCALENDAR" or root.tag == xmlutils.make_clark("CR:addressbook-multiget") and collection.get_meta("tag") != "VADDRESSBOOK" or root.tag == xmlutils.make_clark("D:sync-collection") and collection.get_meta("tag") not in ("VADDRESSBOOK", "VCALENDAR")): logger.warning("Invalid REPORT method %r on %r requested", xmlutils.make_human_tag(root.tag), path) return (client.FORBIDDEN, xmlutils.webdav_error("D:supported-report")) prop_element = root.find(xmlutils.make_clark("D:prop")) props = ([prop.tag for prop in prop_element] if prop_element is not None else []) if root.tag in (xmlutils.make_clark("C:calendar-multiget"), xmlutils.make_clark("CR:addressbook-multiget")): # Read rfc4791-7.9 for info hreferences = set() for href_element in root.findall(xmlutils.make_clark("D:href")): href_path = pathutils.sanitize_path( unquote(urlparse(href_element.text).path)) if (href_path + "/").startswith(base_prefix + "/"): hreferences.add(href_path[len(base_prefix):]) else: logger.warning( "Skipping invalid path %r in REPORT request on " "%r", href_path, path) elif root.tag == xmlutils.make_clark("D:sync-collection"): old_sync_token_element = root.find(xmlutils.make_clark("D:sync-token")) old_sync_token = "" if old_sync_token_element is not None and old_sync_token_element.text: old_sync_token = old_sync_token_element.text.strip() logger.debug("Client provided sync token: %r", old_sync_token) try: sync_token, names = collection.sync(old_sync_token) except ValueError as e: # Invalid sync token logger.warning("Client provided invalid sync token %r: %s", old_sync_token, e, exc_info=True) # client.CONFLICT doesn't work with some clients (e.g. InfCloud) return (client.FORBIDDEN, xmlutils.webdav_error("D:valid-sync-token")) hreferences = (pathutils.unstrip_path( posixpath.join(collection.path, n)) for n in names) # Append current sync token to response sync_token_element = ET.Element(xmlutils.make_clark("D:sync-token")) sync_token_element.text = sync_token multistatus.append(sync_token_element) else: hreferences = (path, ) filters = (root.findall(xmlutils.make_clark("C:filter")) + root.findall(xmlutils.make_clark("CR:filter"))) def retrieve_items(collection, hreferences, multistatus): """Retrieves all items that are referenced in ``hreferences`` from ``collection`` and adds 404 responses for missing and invalid items to ``multistatus``.""" collection_requested = False def get_names(): """Extracts all names from references in ``hreferences`` and adds 404 responses for invalid references to ``multistatus``. If the whole collections is referenced ``collection_requested`` gets set to ``True``.""" nonlocal collection_requested for hreference in hreferences: try: name = pathutils.name_from_path(hreference, collection) except ValueError as e: logger.warning( "Skipping invalid path %r in REPORT request" " on %r: %s", hreference, path, e) response = xml_item_response(base_prefix, hreference, found_item=False) multistatus.append(response) continue if name: # Reference is an item yield name else: # Reference is a collection collection_requested = True for name, item in collection.get_multi(get_names()): if not item: uri = pathutils.unstrip_path( posixpath.join(collection.path, name)) response = xml_item_response(base_prefix, uri, found_item=False) multistatus.append(response) else: yield item, False if collection_requested: yield from collection.get_filtered(filters) # Retrieve everything required for finishing the request. retrieved_items = list(retrieve_items(collection, hreferences, multistatus)) collection_tag = collection.get_meta("tag") # Don't access storage after this! unlock_storage_fn() def match(item, filter_): tag = collection_tag if (tag == "VCALENDAR" and filter_.tag != xmlutils.make_clark("C:%s" % filter_)): if len(filter_) == 0: return True if len(filter_) > 1: raise ValueError("Filter with %d children" % len(filter_)) if filter_[0].tag != xmlutils.make_clark("C:comp-filter"): raise ValueError("Unexpected %r in filter" % filter_[0].tag) return radicale_filter.comp_match(item, filter_[0]) if (tag == "VADDRESSBOOK" and filter_.tag != xmlutils.make_clark("CR:%s" % filter_)): for child in filter_: if child.tag != xmlutils.make_clark("CR:prop-filter"): raise ValueError("Unexpected %r in filter" % child.tag) test = filter_.get("test", "anyof") if test == "anyof": return any( radicale_filter.prop_match(item.vobject_item, f, "CR") for f in filter_) if test == "allof": return all( radicale_filter.prop_match(item.vobject_item, f, "CR") for f in filter_) raise ValueError("Unsupported filter test: %r" % test) raise ValueError("Unsupported filter %r for %r" % (filter_.tag, tag)) while retrieved_items: # ``item.vobject_item`` might be accessed during filtering. # Don't keep reference to ``item``, because VObject requires a lot of # memory. item, filters_matched = retrieved_items.pop(0) if filters and not filters_matched: try: if not all(match(item, filter_) for filter_ in filters): continue except ValueError as e: raise ValueError("Failed to filter item %r from %r: %s" % (item.href, collection.path, e)) from e except Exception as e: raise RuntimeError("Failed to filter item %r from %r: %s" % (item.href, collection.path, e)) from e found_props = [] not_found_props = [] for tag in props: element = ET.Element(tag) if tag == xmlutils.make_clark("D:getetag"): element.text = item.etag found_props.append(element) elif tag == xmlutils.make_clark("D:getcontenttype"): element.text = xmlutils.get_content_type(item, encoding) found_props.append(element) elif tag in (xmlutils.make_clark("C:calendar-data"), xmlutils.make_clark("CR:address-data")): element.text = item.serialize() found_props.append(element) else: not_found_props.append(element) uri = pathutils.unstrip_path(posixpath.join(collection.path, item.href)) multistatus.append( xml_item_response(base_prefix, uri, found_props=found_props, not_found_props=not_found_props, found_item=True)) return client.MULTI_STATUS, multistatus
def do_PUT(self, environ, base_prefix, path, user): """Manage PUT request.""" if not self.access(user, path, "w"): return httputils.NOT_ALLOWED try: content = self.read_content(environ) except RuntimeError as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) permissions = self.Rights.authorized(user, path, "Ww") parent_permissions = self.Rights.authorized(user, parent_path, "w") def prepare(vobject_items, tag=None, write_whole_collection=None): if (write_whole_collection or permissions and not parent_permissions): write_whole_collection = True content_type = environ.get("CONTENT_TYPE", "").split(";")[0] tags = { value: key for key, value in xmlutils.MIMETYPES.items() } tag = radicale_item.predict_tag_of_whole_collection( vobject_items, tags.get(content_type)) if not tag: raise ValueError("Can't determine collection tag") collection_path = pathutils.strip_path(path) elif (write_whole_collection is not None and not write_whole_collection or not permissions and parent_permissions): write_whole_collection = False if tag is None: tag = radicale_item.predict_tag_of_parent_collection( vobject_items) collection_path = posixpath.dirname(pathutils.strip_path(path)) props = None stored_exc_info = None items = [] try: if tag: radicale_item.check_and_sanitize_items( vobject_items, is_collection=write_whole_collection, tag=tag) if write_whole_collection and tag == "VCALENDAR": vobject_components = [] vobject_item, = vobject_items for content in ("vevent", "vtodo", "vjournal"): vobject_components.extend( getattr(vobject_item, "%s_list" % content, [])) vobject_components_by_uid = itertools.groupby( sorted(vobject_components, key=radicale_item.get_uid), radicale_item.get_uid) for uid, components in vobject_components_by_uid: vobject_collection = vobject.iCalendar() for component in components: vobject_collection.add(component) item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_collection) item.prepare() items.append(item) elif write_whole_collection and tag == "VADDRESSBOOK": for vobject_item in vobject_items: item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) elif not write_whole_collection: vobject_item, = vobject_items item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) if write_whole_collection: props = {} if tag: props["tag"] = tag if tag == "VCALENDAR" and vobject_items: if hasattr(vobject_items[0], "x_wr_calname"): calname = vobject_items[0].x_wr_calname.value if calname: props["D:displayname"] = calname if hasattr(vobject_items[0], "x_wr_caldesc"): caldesc = vobject_items[0].x_wr_caldesc.value if caldesc: props["C:calendar-description"] = caldesc radicale_item.check_and_sanitize_props(props) except Exception: stored_exc_info = sys.exc_info() # Use generator for items and delete references to free memory # early def items_generator(): while items: yield items.pop(0) return (items_generator(), tag, write_whole_collection, props, stored_exc_info) try: vobject_items = tuple(vobject.readComponents(content or "")) except Exception as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare(vobject_items) with self.Collection.acquire_lock("w", user): item = next(self.Collection.discover(path), None) parent_item = next(self.Collection.discover(parent_path), None) if not parent_item: return httputils.CONFLICT write_whole_collection = (isinstance(item, storage.BaseCollection) or not parent_item.get_meta("tag")) if write_whole_collection: tag = prepared_tag else: tag = parent_item.get_meta("tag") if write_whole_collection: if not self.Rights.authorized(user, path, "w" if tag else "W"): return httputils.NOT_ALLOWED elif not self.Rights.authorized(user, parent_path, "w"): return httputils.NOT_ALLOWED etag = environ.get("HTTP_IF_MATCH", "") if not item and etag: # Etag asked but no item found: item has been removed return httputils.PRECONDITION_FAILED if item and etag and item.etag != etag: # Etag asked but item not matching: item has changed return httputils.PRECONDITION_FAILED match = environ.get("HTTP_IF_NONE_MATCH", "") == "*" if item and match: # Creation asked but item found: item can't be replaced return httputils.PRECONDITION_FAILED if (tag != prepared_tag or prepared_write_whole_collection != write_whole_collection): (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare(vobject_items, tag, write_whole_collection) props = prepared_props if prepared_exc_info: logger.warning("Bad PUT request on %r: %s", path, prepared_exc_info[1], exc_info=prepared_exc_info) return httputils.BAD_REQUEST if write_whole_collection: try: etag = self.Collection.create_collection( path, prepared_items, props).etag except ValueError as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST else: prepared_item, = prepared_items if (item and item.uid != prepared_item.uid or not item and parent_item.has_uid(prepared_item.uid)): return self.webdav_error_response( "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict") href = posixpath.basename(pathutils.strip_path(path)) try: etag = parent_item.upload(href, prepared_item).etag except ValueError as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST headers = {"ETag": etag} return client.CREATED, headers, None
def xml_propfind_response(base_prefix, path, item, props, user, encoding, write=False, propname=False, allprop=False): """Build and return a PROPFIND response.""" if propname and allprop or (props and (propname or allprop)): raise ValueError("Only use one of props, propname and allprops") is_collection = isinstance(item, storage.BaseCollection) if is_collection: is_leaf = item.get_meta("tag") in ("VADDRESSBOOK", "VCALENDAR") collection = item else: collection = item.collection response = ET.Element(xmlutils.make_clark("D:response")) href = ET.Element(xmlutils.make_clark("D:href")) if is_collection: # Some clients expect collections to end with / uri = pathutils.unstrip_path(item.path, True) else: uri = pathutils.unstrip_path(posixpath.join(collection.path, item.href)) href.text = xmlutils.make_href(base_prefix, uri) response.append(href) if propname or allprop: props = [] # Should list all properties that can be retrieved by the code below props.append(xmlutils.make_clark("D:principal-collection-set")) props.append(xmlutils.make_clark("D:current-user-principal")) props.append(xmlutils.make_clark("D:current-user-privilege-set")) props.append(xmlutils.make_clark("D:supported-report-set")) props.append(xmlutils.make_clark("D:resourcetype")) props.append(xmlutils.make_clark("D:owner")) if is_collection and collection.is_principal: props.append(xmlutils.make_clark("C:calendar-user-address-set")) props.append(xmlutils.make_clark("D:principal-URL")) props.append(xmlutils.make_clark("CR:addressbook-home-set")) props.append(xmlutils.make_clark("C:calendar-home-set")) if not is_collection or is_leaf: props.append(xmlutils.make_clark("D:getetag")) props.append(xmlutils.make_clark("D:getlastmodified")) props.append(xmlutils.make_clark("D:getcontenttype")) props.append(xmlutils.make_clark("D:getcontentlength")) if is_collection: if is_leaf: props.append(xmlutils.make_clark("D:displayname")) props.append(xmlutils.make_clark("D:sync-token")) if collection.get_meta("tag") == "VCALENDAR": props.append(xmlutils.make_clark("CS:getctag")) props.append( xmlutils.make_clark("C:supported-calendar-component-set")) meta = item.get_meta() for tag in meta: if tag == "tag": continue clark_tag = xmlutils.make_clark(tag) if clark_tag not in props: props.append(clark_tag) responses = collections.defaultdict(list) if propname: for tag in props: responses[200].append(ET.Element(tag)) props = () for tag in props: element = ET.Element(tag) is404 = False if tag == xmlutils.make_clark("D:getetag"): if not is_collection or is_leaf: element.text = item.etag else: is404 = True elif tag == xmlutils.make_clark("D:getlastmodified"): if not is_collection or is_leaf: element.text = item.last_modified else: is404 = True elif tag == xmlutils.make_clark("D:principal-collection-set"): child_element = ET.Element(xmlutils.make_clark("D:href")) child_element.text = xmlutils.make_href(base_prefix, "/") element.append(child_element) elif (tag in (xmlutils.make_clark("C:calendar-user-address-set"), xmlutils.make_clark("D:principal-URL"), xmlutils.make_clark("CR:addressbook-home-set"), xmlutils.make_clark("C:calendar-home-set")) and collection.is_principal and is_collection): child_element = ET.Element(xmlutils.make_clark("D:href")) child_element.text = xmlutils.make_href(base_prefix, path) element.append(child_element) elif tag == xmlutils.make_clark("C:supported-calendar-component-set"): human_tag = xmlutils.make_human_tag(tag) if is_collection and is_leaf: meta = item.get_meta(human_tag) if meta: components = meta.split(",") else: components = ("VTODO", "VEVENT", "VJOURNAL") for component in components: comp = ET.Element(xmlutils.make_clark("C:comp")) comp.set("name", component) element.append(comp) else: is404 = True elif tag == xmlutils.make_clark("D:current-user-principal"): if user: child_element = ET.Element(xmlutils.make_clark("D:href")) child_element.text = xmlutils.make_href( base_prefix, "/%s/" % user) element.append(child_element) else: element.append( ET.Element(xmlutils.make_clark("D:unauthenticated"))) elif tag == xmlutils.make_clark("D:current-user-privilege-set"): privileges = ["D:read"] if write: privileges.append("D:all") privileges.append("D:write") privileges.append("D:write-properties") privileges.append("D:write-content") for human_tag in privileges: privilege = ET.Element(xmlutils.make_clark("D:privilege")) privilege.append(ET.Element(xmlutils.make_clark(human_tag))) element.append(privilege) elif tag == xmlutils.make_clark("D:supported-report-set"): # These 3 reports are not implemented reports = [ "D:expand-property", "D:principal-search-property-set", "D:principal-property-search" ] if is_collection and is_leaf: reports.append("D:sync-collection") if item.get_meta("tag") == "VADDRESSBOOK": reports.append("CR:addressbook-multiget") reports.append("CR:addressbook-query") elif item.get_meta("tag") == "VCALENDAR": reports.append("C:calendar-multiget") reports.append("C:calendar-query") for human_tag in reports: supported_report = ET.Element( xmlutils.make_clark("D:supported-report")) report_element = ET.Element(xmlutils.make_clark("D:report")) report_element.append( ET.Element(xmlutils.make_clark(human_tag))) supported_report.append(report_element) element.append(supported_report) elif tag == xmlutils.make_clark("D:getcontentlength"): if not is_collection or is_leaf: element.text = str(len(item.serialize().encode(encoding))) else: is404 = True elif tag == xmlutils.make_clark("D:owner"): # return empty elment, if no owner available (rfc3744-5.1) if collection.owner: child_element = ET.Element(xmlutils.make_clark("D:href")) child_element.text = xmlutils.make_href( base_prefix, "/%s/" % collection.owner) element.append(child_element) elif is_collection: if tag == xmlutils.make_clark("D:getcontenttype"): if is_leaf: element.text = xmlutils.MIMETYPES[item.get_meta("tag")] else: is404 = True elif tag == xmlutils.make_clark("D:resourcetype"): if item.is_principal: child_element = ET.Element( xmlutils.make_clark("D:principal")) element.append(child_element) if is_leaf: if item.get_meta("tag") == "VADDRESSBOOK": child_element = ET.Element( xmlutils.make_clark("CR:addressbook")) element.append(child_element) elif item.get_meta("tag") == "VCALENDAR": child_element = ET.Element( xmlutils.make_clark("C:calendar")) element.append(child_element) child_element = ET.Element(xmlutils.make_clark("D:collection")) element.append(child_element) elif tag == xmlutils.make_clark("RADICALE:displayname"): # Only for internal use by the web interface displayname = item.get_meta("D:displayname") if displayname is not None: element.text = displayname else: is404 = True elif tag == xmlutils.make_clark("D:displayname"): displayname = item.get_meta("D:displayname") if not displayname and is_leaf: displayname = item.path if displayname is not None: element.text = displayname else: is404 = True elif tag == xmlutils.make_clark("CS:getctag"): if is_leaf: element.text = item.etag else: is404 = True elif tag == xmlutils.make_clark("D:sync-token"): if is_leaf: element.text, _ = item.sync() else: is404 = True else: human_tag = xmlutils.make_human_tag(tag) meta = item.get_meta(human_tag) if meta is not None: element.text = meta else: is404 = True # Not for collections elif tag == xmlutils.make_clark("D:getcontenttype"): element.text = xmlutils.get_content_type(item, encoding) elif tag == xmlutils.make_clark("D:resourcetype"): # resourcetype must be returned empty for non-collection elements pass else: is404 = True responses[404 if is404 else 200].append(element) for status_code, childs in responses.items(): if not childs: continue propstat = ET.Element(xmlutils.make_clark("D:propstat")) response.append(propstat) prop = ET.Element(xmlutils.make_clark("D:prop")) prop.extend(childs) propstat.append(prop) status = ET.Element(xmlutils.make_clark("D:status")) status.text = xmlutils.make_response(status_code) propstat.append(status) return response
def do_PUT(self, environ, base_prefix, path, user): """Manage PUT request.""" if not self._access(user, path, "w"): return httputils.NOT_ALLOWED try: content = self._read_content(environ) except RuntimeError as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking content_type = environ.get("CONTENT_TYPE", "").split(";")[0] parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) permissions = rights.intersect( self._rights.authorization(user, path), "Ww") parent_permissions = rights.intersect( self._rights.authorization(user, parent_path), "w") try: vobject_items = tuple(vobject.readComponents(content or "")) except Exception as e: logger.warning( "Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare( vobject_items, path, content_type, permissions, parent_permissions) with self._storage.acquire_lock("w", user): item = next(self._storage.discover(path), None) parent_item = next(self._storage.discover(parent_path), None) if not parent_item: return httputils.CONFLICT write_whole_collection = ( isinstance(item, storage.BaseCollection) or not parent_item.get_meta("tag")) if write_whole_collection: tag = prepared_tag else: tag = parent_item.get_meta("tag") if write_whole_collection: if ("w" if tag else "W") not in self._rights.authorization( user, path): return httputils.NOT_ALLOWED elif "w" not in self._rights.authorization(user, parent_path): return httputils.NOT_ALLOWED etag = environ.get("HTTP_IF_MATCH", "") if not item and etag: # Etag asked but no item found: item has been removed return httputils.PRECONDITION_FAILED if item and etag and item.etag != etag: # Etag asked but item not matching: item has changed return httputils.PRECONDITION_FAILED match = environ.get("HTTP_IF_NONE_MATCH", "") == "*" if item and match: # Creation asked but item found: item can't be replaced return httputils.PRECONDITION_FAILED if (tag != prepared_tag or prepared_write_whole_collection != write_whole_collection): (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare( vobject_items, path, content_type, permissions, parent_permissions, tag, write_whole_collection) props = prepared_props if prepared_exc_info: logger.warning( "Bad PUT request on %r: %s", path, prepared_exc_info[1], exc_info=prepared_exc_info) return httputils.BAD_REQUEST if write_whole_collection: try: etag = self._storage.create_collection( path, prepared_items, props).etag except ValueError as e: logger.warning( "Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST else: prepared_item, = prepared_items if (item and item.uid != prepared_item.uid or not item and parent_item.has_uid(prepared_item.uid)): return self._webdav_error_response("%s:no-uid-conflict" % ( "C" if tag == "VCALENDAR" else "CR")) href = posixpath.basename(pathutils.strip_path(path)) try: etag = parent_item.upload(href, prepared_item).etag except ValueError as e: logger.warning( "Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST headers = {"ETag": etag} return client.CREATED, headers, None
def xml_propfind_response(base_prefix, path, item, props, user, write=False, propname=False, allprop=False): """Build and return a PROPFIND response.""" if propname and allprop or (props and (propname or allprop)): raise ValueError("Only use one of props, propname and allprops") is_collection = isinstance(item, storage.BaseCollection) if is_collection: is_leaf = item.get_meta("tag") in ("VADDRESSBOOK", "VCALENDAR") collection = item else: collection = item.collection response = ET.Element(xmlutils.make_tag("D", "response")) href = ET.Element(xmlutils.make_tag("D", "href")) if is_collection: # Some clients expect collections to end with / uri = pathutils.unstrip_path(item.path, True) else: uri = pathutils.unstrip_path( posixpath.join(collection.path, item.href)) href.text = xmlutils.make_href(base_prefix, uri) response.append(href) propstat404 = ET.Element(xmlutils.make_tag("D", "propstat")) propstat200 = ET.Element(xmlutils.make_tag("D", "propstat")) response.append(propstat200) prop200 = ET.Element(xmlutils.make_tag("D", "prop")) propstat200.append(prop200) prop404 = ET.Element(xmlutils.make_tag("D", "prop")) propstat404.append(prop404) if propname or allprop: props = [] # Should list all properties that can be retrieved by the code below props.append(xmlutils.make_tag("D", "principal-collection-set")) props.append(xmlutils.make_tag("D", "current-user-principal")) props.append(xmlutils.make_tag("D", "current-user-privilege-set")) props.append(xmlutils.make_tag("D", "supported-report-set")) props.append(xmlutils.make_tag("D", "resourcetype")) props.append(xmlutils.make_tag("D", "owner")) if is_collection and collection.is_principal: props.append(xmlutils.make_tag("C", "calendar-user-address-set")) props.append(xmlutils.make_tag("D", "principal-URL")) props.append(xmlutils.make_tag("CR", "addressbook-home-set")) props.append(xmlutils.make_tag("C", "calendar-home-set")) if not is_collection or is_leaf: props.append(xmlutils.make_tag("D", "getetag")) props.append(xmlutils.make_tag("D", "getlastmodified")) props.append(xmlutils.make_tag("D", "getcontenttype")) props.append(xmlutils.make_tag("D", "getcontentlength")) if is_collection: if is_leaf: props.append(xmlutils.make_tag("D", "displayname")) props.append(xmlutils.make_tag("D", "sync-token")) if collection.get_meta("tag") == "VCALENDAR": props.append(xmlutils.make_tag("CS", "getctag")) props.append( xmlutils.make_tag("C", "supported-calendar-component-set")) meta = item.get_meta() for tag in meta: if tag == "tag": continue clark_tag = xmlutils.tag_from_human(tag) if clark_tag not in props: props.append(clark_tag) if propname: for tag in props: prop200.append(ET.Element(tag)) props = () for tag in props: element = ET.Element(tag) is404 = False if tag == xmlutils.make_tag("D", "getetag"): if not is_collection or is_leaf: element.text = item.etag else: is404 = True elif tag == xmlutils.make_tag("D", "getlastmodified"): if not is_collection or is_leaf: element.text = item.last_modified else: is404 = True elif tag == xmlutils.make_tag("D", "principal-collection-set"): tag = ET.Element(xmlutils.make_tag("D", "href")) tag.text = xmlutils.make_href(base_prefix, "/") element.append(tag) elif (tag in (xmlutils.make_tag("C", "calendar-user-address-set"), xmlutils.make_tag("D", "principal-URL"), xmlutils.make_tag("CR", "addressbook-home-set"), xmlutils.make_tag("C", "calendar-home-set")) and collection.is_principal and is_collection): tag = ET.Element(xmlutils.make_tag("D", "href")) tag.text = xmlutils.make_href(base_prefix, path) element.append(tag) elif tag == xmlutils.make_tag("C", "supported-calendar-component-set"): human_tag = xmlutils.tag_from_clark(tag) if is_collection and is_leaf: meta = item.get_meta(human_tag) if meta: components = meta.split(",") else: components = ("VTODO", "VEVENT", "VJOURNAL") for component in components: comp = ET.Element(xmlutils.make_tag("C", "comp")) comp.set("name", component) element.append(comp) else: is404 = True elif tag == xmlutils.make_tag("D", "current-user-principal"): if user: tag = ET.Element(xmlutils.make_tag("D", "href")) tag.text = xmlutils.make_href(base_prefix, "/%s/" % user) element.append(tag) else: element.append(ET.Element( xmlutils.make_tag("D", "unauthenticated"))) elif tag == xmlutils.make_tag("D", "current-user-privilege-set"): privileges = [("D", "read")] if write: privileges.append(("D", "all")) privileges.append(("D", "write")) privileges.append(("D", "write-properties")) privileges.append(("D", "write-content")) for ns, privilege_name in privileges: privilege = ET.Element(xmlutils.make_tag("D", "privilege")) privilege.append(ET.Element( xmlutils.make_tag(ns, privilege_name))) element.append(privilege) elif tag == xmlutils.make_tag("D", "supported-report-set"): # These 3 reports are not implemented reports = [ ("D", "expand-property"), ("D", "principal-search-property-set"), ("D", "principal-property-search")] if is_collection and is_leaf: reports.append(("D", "sync-collection")) if item.get_meta("tag") == "VADDRESSBOOK": reports.append(("CR", "addressbook-multiget")) reports.append(("CR", "addressbook-query")) elif item.get_meta("tag") == "VCALENDAR": reports.append(("C", "calendar-multiget")) reports.append(("C", "calendar-query")) for ns, report_name in reports: supported = ET.Element( xmlutils.make_tag("D", "supported-report")) report_tag = ET.Element(xmlutils.make_tag("D", "report")) supported_report_tag = ET.Element( xmlutils.make_tag(ns, report_name)) report_tag.append(supported_report_tag) supported.append(report_tag) element.append(supported) elif tag == xmlutils.make_tag("D", "getcontentlength"): if not is_collection or is_leaf: encoding = collection.configuration.get("encoding", "request") element.text = str(len(item.serialize().encode(encoding))) else: is404 = True elif tag == xmlutils.make_tag("D", "owner"): # return empty elment, if no owner available (rfc3744-5.1) if collection.owner: tag = ET.Element(xmlutils.make_tag("D", "href")) tag.text = xmlutils.make_href( base_prefix, "/%s/" % collection.owner) element.append(tag) elif is_collection: if tag == xmlutils.make_tag("D", "getcontenttype"): if is_leaf: element.text = xmlutils.MIMETYPES[item.get_meta("tag")] else: is404 = True elif tag == xmlutils.make_tag("D", "resourcetype"): if item.is_principal: tag = ET.Element(xmlutils.make_tag("D", "principal")) element.append(tag) if is_leaf: if item.get_meta("tag") == "VADDRESSBOOK": tag = ET.Element( xmlutils.make_tag("CR", "addressbook")) element.append(tag) elif item.get_meta("tag") == "VCALENDAR": tag = ET.Element(xmlutils.make_tag("C", "calendar")) element.append(tag) tag = ET.Element(xmlutils.make_tag("D", "collection")) element.append(tag) elif tag == xmlutils.make_tag("RADICALE", "displayname"): # Only for internal use by the web interface displayname = item.get_meta("D:displayname") if displayname is not None: element.text = displayname else: is404 = True elif tag == xmlutils.make_tag("D", "displayname"): displayname = item.get_meta("D:displayname") if not displayname and is_leaf: displayname = item.path if displayname is not None: element.text = displayname else: is404 = True elif tag == xmlutils.make_tag("CS", "getctag"): if is_leaf: element.text = item.etag else: is404 = True elif tag == xmlutils.make_tag("D", "sync-token"): if is_leaf: element.text, _ = item.sync() else: is404 = True else: human_tag = xmlutils.tag_from_clark(tag) meta = item.get_meta(human_tag) if meta is not None: element.text = meta else: is404 = True # Not for collections elif tag == xmlutils.make_tag("D", "getcontenttype"): element.text = xmlutils.get_content_type(item) elif tag == xmlutils.make_tag("D", "resourcetype"): # resourcetype must be returned empty for non-collection elements pass else: is404 = True if is404: prop404.append(element) else: prop200.append(element) status200 = ET.Element(xmlutils.make_tag("D", "status")) status200.text = xmlutils.make_response(200) propstat200.append(status200) status404 = ET.Element(xmlutils.make_tag("D", "status")) status404.text = xmlutils.make_response(404) propstat404.append(status404) if len(prop404): response.append(propstat404) return response
def xml_report(base_prefix, path, xml_request, collection, unlock_storage_fn): """Read and answer REPORT requests. Read rfc3253-3.6 for info. """ multistatus = ET.Element(xmlutils.make_tag("D", "multistatus")) if xml_request is None: return client.MULTI_STATUS, multistatus root = xml_request if root.tag in ( xmlutils.make_tag("D", "principal-search-property-set"), xmlutils.make_tag("D", "principal-property-search"), xmlutils.make_tag("D", "expand-property")): # We don't support searching for principals or indirect retrieving of # properties, just return an empty result. # InfCloud asks for expand-property reports (even if we don't announce # support for them) and stops working if an error code is returned. logger.warning("Unsupported REPORT method %r on %r requested", xmlutils.tag_from_clark(root.tag), path) return client.MULTI_STATUS, multistatus if (root.tag == xmlutils.make_tag("C", "calendar-multiget") and collection.get_meta("tag") != "VCALENDAR" or root.tag == xmlutils.make_tag("CR", "addressbook-multiget") and collection.get_meta("tag") != "VADDRESSBOOK" or root.tag == xmlutils.make_tag("D", "sync-collection") and collection.get_meta("tag") not in ("VADDRESSBOOK", "VCALENDAR")): logger.warning("Invalid REPORT method %r on %r requested", xmlutils.tag_from_clark(root.tag), path) return (client.CONFLICT, xmlutils.webdav_error("D", "supported-report")) prop_element = root.find(xmlutils.make_tag("D", "prop")) props = ( [prop.tag for prop in prop_element] if prop_element is not None else []) if root.tag in ( xmlutils.make_tag("C", "calendar-multiget"), xmlutils.make_tag("CR", "addressbook-multiget")): # Read rfc4791-7.9 for info hreferences = set() for href_element in root.findall(xmlutils.make_tag("D", "href")): href_path = pathutils.sanitize_path( unquote(urlparse(href_element.text).path)) if (href_path + "/").startswith(base_prefix + "/"): hreferences.add(href_path[len(base_prefix):]) else: logger.warning("Skipping invalid path %r in REPORT request on " "%r", href_path, path) elif root.tag == xmlutils.make_tag("D", "sync-collection"): old_sync_token_element = root.find( xmlutils.make_tag("D", "sync-token")) old_sync_token = "" if old_sync_token_element is not None and old_sync_token_element.text: old_sync_token = old_sync_token_element.text.strip() logger.debug("Client provided sync token: %r", old_sync_token) try: sync_token, names = collection.sync(old_sync_token) except ValueError as e: # Invalid sync token logger.warning("Client provided invalid sync token %r: %s", old_sync_token, e, exc_info=True) return (client.CONFLICT, xmlutils.webdav_error("D", "valid-sync-token")) hreferences = (pathutils.unstrip_path( posixpath.join(collection.path, n)) for n in names) # Append current sync token to response sync_token_element = ET.Element(xmlutils.make_tag("D", "sync-token")) sync_token_element.text = sync_token multistatus.append(sync_token_element) else: hreferences = (path,) filters = ( root.findall("./%s" % xmlutils.make_tag("C", "filter")) + root.findall("./%s" % xmlutils.make_tag("CR", "filter"))) def retrieve_items(collection, hreferences, multistatus): """Retrieves all items that are referenced in ``hreferences`` from ``collection`` and adds 404 responses for missing and invalid items to ``multistatus``.""" collection_requested = False def get_names(): """Extracts all names from references in ``hreferences`` and adds 404 responses for invalid references to ``multistatus``. If the whole collections is referenced ``collection_requested`` gets set to ``True``.""" nonlocal collection_requested for hreference in hreferences: try: name = pathutils.name_from_path(hreference, collection) except ValueError as e: logger.warning("Skipping invalid path %r in REPORT request" " on %r: %s", hreference, path, e) response = xml_item_response(base_prefix, hreference, found_item=False) multistatus.append(response) continue if name: # Reference is an item yield name else: # Reference is a collection collection_requested = True for name, item in collection.get_multi(get_names()): if not item: uri = pathutils.unstrip_path( posixpath.join(collection.path, name)) response = xml_item_response(base_prefix, uri, found_item=False) multistatus.append(response) else: yield item, False if collection_requested: yield from collection.get_filtered(filters) # Retrieve everything required for finishing the request. retrieved_items = list(retrieve_items(collection, hreferences, multistatus)) collection_tag = collection.get_meta("tag") # Don't access storage after this! unlock_storage_fn() def match(item, filter_): tag = collection_tag if (tag == "VCALENDAR" and filter_.tag != xmlutils.make_tag("C", filter_)): if len(filter_) == 0: return True if len(filter_) > 1: raise ValueError("Filter with %d children" % len(filter_)) if filter_[0].tag != xmlutils.make_tag("C", "comp-filter"): raise ValueError("Unexpected %r in filter" % filter_[0].tag) return radicale_filter.comp_match(item, filter_[0]) if (tag == "VADDRESSBOOK" and filter_.tag != xmlutils.make_tag("CR", filter_)): for child in filter_: if child.tag != xmlutils.make_tag("CR", "prop-filter"): raise ValueError("Unexpected %r in filter" % child.tag) test = filter_.get("test", "anyof") if test == "anyof": return any( radicale_filter.prop_match(item.vobject_item, f, "CR") for f in filter_) if test == "allof": return all( radicale_filter.prop_match(item.vobject_item, f, "CR") for f in filter_) raise ValueError("Unsupported filter test: %r" % test) return all(radicale_filter.prop_match(item.vobject_item, f, "CR") for f in filter_) raise ValueError("unsupported filter %r for %r" % (filter_.tag, tag)) while retrieved_items: # ``item.vobject_item`` might be accessed during filtering. # Don't keep reference to ``item``, because VObject requires a lot of # memory. item, filters_matched = retrieved_items.pop(0) if filters and not filters_matched: try: if not all(match(item, filter_) for filter_ in filters): continue except ValueError as e: raise ValueError("Failed to filter item %r from %r: %s" % (item.href, collection.path, e)) from e except Exception as e: raise RuntimeError("Failed to filter item %r from %r: %s" % (item.href, collection.path, e)) from e found_props = [] not_found_props = [] for tag in props: element = ET.Element(tag) if tag == xmlutils.make_tag("D", "getetag"): element.text = item.etag found_props.append(element) elif tag == xmlutils.make_tag("D", "getcontenttype"): element.text = xmlutils.get_content_type(item) found_props.append(element) elif tag in ( xmlutils.make_tag("C", "calendar-data"), xmlutils.make_tag("CR", "address-data")): element.text = item.serialize() found_props.append(element) else: not_found_props.append(element) uri = pathutils.unstrip_path( posixpath.join(collection.path, item.href)) multistatus.append(xml_item_response( base_prefix, uri, found_props=found_props, not_found_props=not_found_props, found_item=True)) return client.MULTI_STATUS, multistatus
def xml_propfind_response(base_prefix, path, item, props, user, write=False, propname=False, allprop=False): """Build and return a PROPFIND response.""" if propname and allprop or (props and (propname or allprop)): raise ValueError("Only use one of props, propname and allprops") is_collection = isinstance(item, storage.BaseCollection) if is_collection: is_leaf = item.get_meta("tag") in ("VADDRESSBOOK", "VCALENDAR") collection = item else: collection = item.collection response = ET.Element(xmlutils.make_tag("D", "response")) href = ET.Element(xmlutils.make_tag("D", "href")) if is_collection: # Some clients expect collections to end with / uri = pathutils.unstrip_path(item.path, True) else: uri = pathutils.unstrip_path(posixpath.join(collection.path, item.href)) href.text = xmlutils.make_href(base_prefix, uri) response.append(href) propstat404 = ET.Element(xmlutils.make_tag("D", "propstat")) propstat200 = ET.Element(xmlutils.make_tag("D", "propstat")) response.append(propstat200) prop200 = ET.Element(xmlutils.make_tag("D", "prop")) propstat200.append(prop200) prop404 = ET.Element(xmlutils.make_tag("D", "prop")) propstat404.append(prop404) if propname or allprop: props = [] # Should list all properties that can be retrieved by the code below props.append(xmlutils.make_tag("D", "principal-collection-set")) props.append(xmlutils.make_tag("D", "current-user-principal")) props.append(xmlutils.make_tag("D", "current-user-privilege-set")) props.append(xmlutils.make_tag("D", "supported-report-set")) props.append(xmlutils.make_tag("D", "resourcetype")) props.append(xmlutils.make_tag("D", "owner")) if is_collection and collection.is_principal: props.append(xmlutils.make_tag("C", "calendar-user-address-set")) props.append(xmlutils.make_tag("D", "principal-URL")) props.append(xmlutils.make_tag("CR", "addressbook-home-set")) props.append(xmlutils.make_tag("C", "calendar-home-set")) if not is_collection or is_leaf: props.append(xmlutils.make_tag("D", "getetag")) props.append(xmlutils.make_tag("D", "getlastmodified")) props.append(xmlutils.make_tag("D", "getcontenttype")) props.append(xmlutils.make_tag("D", "getcontentlength")) if is_collection: if is_leaf: props.append(xmlutils.make_tag("D", "displayname")) props.append(xmlutils.make_tag("D", "sync-token")) if collection.get_meta("tag") == "VCALENDAR": props.append(xmlutils.make_tag("CS", "getctag")) props.append( xmlutils.make_tag("C", "supported-calendar-component-set")) meta = item.get_meta() for tag in meta: if tag == "tag": continue clark_tag = xmlutils.tag_from_human(tag) if clark_tag not in props: props.append(clark_tag) if propname: for tag in props: prop200.append(ET.Element(tag)) props = () for tag in props: element = ET.Element(tag) is404 = False if tag == xmlutils.make_tag("D", "getetag"): if not is_collection or is_leaf: element.text = item.etag else: is404 = True elif tag == xmlutils.make_tag("D", "getlastmodified"): if not is_collection or is_leaf: element.text = item.last_modified else: is404 = True elif tag == xmlutils.make_tag("D", "principal-collection-set"): tag = ET.Element(xmlutils.make_tag("D", "href")) tag.text = xmlutils.make_href(base_prefix, "/") element.append(tag) elif (tag in (xmlutils.make_tag("C", "calendar-user-address-set"), xmlutils.make_tag("D", "principal-URL"), xmlutils.make_tag("CR", "addressbook-home-set"), xmlutils.make_tag("C", "calendar-home-set")) and collection.is_principal and is_collection): tag = ET.Element(xmlutils.make_tag("D", "href")) tag.text = xmlutils.make_href(base_prefix, path) element.append(tag) elif tag == xmlutils.make_tag("C", "supported-calendar-component-set"): human_tag = xmlutils.tag_from_clark(tag) if is_collection and is_leaf: meta = item.get_meta(human_tag) if meta: components = meta.split(",") else: components = ("VTODO", "VEVENT", "VJOURNAL") for component in components: comp = ET.Element(xmlutils.make_tag("C", "comp")) comp.set("name", component) element.append(comp) else: is404 = True elif tag == xmlutils.make_tag("D", "current-user-principal"): if user: tag = ET.Element(xmlutils.make_tag("D", "href")) tag.text = xmlutils.make_href(base_prefix, "/%s/" % user) element.append(tag) else: element.append( ET.Element(xmlutils.make_tag("D", "unauthenticated"))) elif tag == xmlutils.make_tag("D", "current-user-privilege-set"): privileges = [("D", "read")] if write: privileges.append(("D", "all")) privileges.append(("D", "write")) privileges.append(("D", "write-properties")) privileges.append(("D", "write-content")) for ns, privilege_name in privileges: privilege = ET.Element(xmlutils.make_tag("D", "privilege")) privilege.append( ET.Element(xmlutils.make_tag(ns, privilege_name))) element.append(privilege) elif tag == xmlutils.make_tag("D", "supported-report-set"): # These 3 reports are not implemented reports = [("D", "expand-property"), ("D", "principal-search-property-set"), ("D", "principal-property-search")] if is_collection and is_leaf: reports.append(("D", "sync-collection")) if item.get_meta("tag") == "VADDRESSBOOK": reports.append(("CR", "addressbook-multiget")) reports.append(("CR", "addressbook-query")) elif item.get_meta("tag") == "VCALENDAR": reports.append(("C", "calendar-multiget")) reports.append(("C", "calendar-query")) for ns, report_name in reports: supported = ET.Element( xmlutils.make_tag("D", "supported-report")) report_tag = ET.Element(xmlutils.make_tag("D", "report")) supported_report_tag = ET.Element( xmlutils.make_tag(ns, report_name)) report_tag.append(supported_report_tag) supported.append(report_tag) element.append(supported) elif tag == xmlutils.make_tag("D", "getcontentlength"): if not is_collection or is_leaf: encoding = collection.configuration.get("encoding", "request") element.text = str(len(item.serialize().encode(encoding))) else: is404 = True elif tag == xmlutils.make_tag("D", "owner"): # return empty elment, if no owner available (rfc3744-5.1) if collection.owner: tag = ET.Element(xmlutils.make_tag("D", "href")) tag.text = xmlutils.make_href(base_prefix, "/%s/" % collection.owner) element.append(tag) elif is_collection: if tag == xmlutils.make_tag("D", "getcontenttype"): if is_leaf: element.text = xmlutils.MIMETYPES[item.get_meta("tag")] else: is404 = True elif tag == xmlutils.make_tag("D", "resourcetype"): if item.is_principal: tag = ET.Element(xmlutils.make_tag("D", "principal")) element.append(tag) if is_leaf: if item.get_meta("tag") == "VADDRESSBOOK": tag = ET.Element(xmlutils.make_tag( "CR", "addressbook")) element.append(tag) elif item.get_meta("tag") == "VCALENDAR": tag = ET.Element(xmlutils.make_tag("C", "calendar")) element.append(tag) tag = ET.Element(xmlutils.make_tag("D", "collection")) element.append(tag) elif tag == xmlutils.make_tag("RADICALE", "displayname"): # Only for internal use by the web interface displayname = item.get_meta("D:displayname") if displayname is not None: element.text = displayname else: is404 = True elif tag == xmlutils.make_tag("D", "displayname"): displayname = item.get_meta("D:displayname") if not displayname and is_leaf: displayname = item.path if displayname is not None: element.text = displayname else: is404 = True elif tag == xmlutils.make_tag("CS", "getctag"): if is_leaf: element.text = item.etag else: is404 = True elif tag == xmlutils.make_tag("D", "sync-token"): if is_leaf: element.text, _ = item.sync() else: is404 = True else: human_tag = xmlutils.tag_from_clark(tag) meta = item.get_meta(human_tag) if meta is not None: element.text = meta else: is404 = True # Not for collections elif tag == xmlutils.make_tag("D", "getcontenttype"): element.text = xmlutils.get_content_type(item) elif tag == xmlutils.make_tag("D", "resourcetype"): # resourcetype must be returned empty for non-collection elements pass else: is404 = True if is404: prop404.append(element) else: prop200.append(element) status200 = ET.Element(xmlutils.make_tag("D", "status")) status200.text = xmlutils.make_response(200) propstat200.append(status200) status404 = ET.Element(xmlutils.make_tag("D", "status")) status404.text = xmlutils.make_response(404) propstat404.append(status404) if len(prop404): response.append(propstat404) return response
def do_PUT(self, environ, base_prefix, path, user): """Manage PUT request.""" if not self.access(user, path, "w"): return httputils.NOT_ALLOWED try: content = self.read_content(environ) except RuntimeError as e: logger.warning("Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST except socket.timeout: logger.debug("client timed out", exc_info=True) return httputils.REQUEST_TIMEOUT # Prepare before locking parent_path = pathutils.unstrip_path( posixpath.dirname(pathutils.strip_path(path)), True) permissions = self.Rights.authorized(user, path, "Ww") parent_permissions = self.Rights.authorized(user, parent_path, "w") def prepare(vobject_items, tag=None, write_whole_collection=None): if (write_whole_collection or permissions and not parent_permissions): write_whole_collection = True content_type = environ.get("CONTENT_TYPE", "").split(";")[0] tags = {value: key for key, value in xmlutils.MIMETYPES.items()} tag = radicale_item.predict_tag_of_whole_collection( vobject_items, tags.get(content_type)) if not tag: raise ValueError("Can't determine collection tag") collection_path = pathutils.strip_path(path) elif (write_whole_collection is not None and not write_whole_collection or not permissions and parent_permissions): write_whole_collection = False if tag is None: tag = radicale_item.predict_tag_of_parent_collection( vobject_items) collection_path = posixpath.dirname( pathutils.strip_path(path)) props = None stored_exc_info = None items = [] try: if tag: radicale_item.check_and_sanitize_items( vobject_items, is_collection=write_whole_collection, tag=tag) if write_whole_collection and tag == "VCALENDAR": vobject_components = [] vobject_item, = vobject_items for content in ("vevent", "vtodo", "vjournal"): vobject_components.extend( getattr(vobject_item, "%s_list" % content, [])) vobject_components_by_uid = itertools.groupby( sorted(vobject_components, key=radicale_item.get_uid), radicale_item.get_uid) for uid, components in vobject_components_by_uid: vobject_collection = vobject.iCalendar() for component in components: vobject_collection.add(component) item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_collection) item.prepare() items.append(item) elif write_whole_collection and tag == "VADDRESSBOOK": for vobject_item in vobject_items: item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) elif not write_whole_collection: vobject_item, = vobject_items item = radicale_item.Item( collection_path=collection_path, vobject_item=vobject_item) item.prepare() items.append(item) if write_whole_collection: props = {} if tag: props["tag"] = tag if tag == "VCALENDAR" and vobject_items: if hasattr(vobject_items[0], "x_wr_calname"): calname = vobject_items[0].x_wr_calname.value if calname: props["D:displayname"] = calname if hasattr(vobject_items[0], "x_wr_caldesc"): caldesc = vobject_items[0].x_wr_caldesc.value if caldesc: props["C:calendar-description"] = caldesc radicale_item.check_and_sanitize_props(props) except Exception: stored_exc_info = sys.exc_info() # Use generator for items and delete references to free memory # early def items_generator(): while items: yield items.pop(0) return (items_generator(), tag, write_whole_collection, props, stored_exc_info) try: vobject_items = tuple(vobject.readComponents(content or "")) except Exception as e: logger.warning( "Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare(vobject_items) with self.Collection.acquire_lock("w", user): item = next(self.Collection.discover(path), None) parent_item = next(self.Collection.discover(parent_path), None) if not parent_item: return httputils.CONFLICT write_whole_collection = ( isinstance(item, storage.BaseCollection) or not parent_item.get_meta("tag")) if write_whole_collection: tag = prepared_tag else: tag = parent_item.get_meta("tag") if write_whole_collection: if not self.Rights.authorized(user, path, "w" if tag else "W"): return httputils.NOT_ALLOWED elif not self.Rights.authorized(user, parent_path, "w"): return httputils.NOT_ALLOWED etag = environ.get("HTTP_IF_MATCH", "") if not item and etag: # Etag asked but no item found: item has been removed return httputils.PRECONDITION_FAILED if item and etag and item.etag != etag: # Etag asked but item not matching: item has changed return httputils.PRECONDITION_FAILED match = environ.get("HTTP_IF_NONE_MATCH", "") == "*" if item and match: # Creation asked but item found: item can't be replaced return httputils.PRECONDITION_FAILED if (tag != prepared_tag or prepared_write_whole_collection != write_whole_collection): (prepared_items, prepared_tag, prepared_write_whole_collection, prepared_props, prepared_exc_info) = prepare( vobject_items, tag, write_whole_collection) props = prepared_props if prepared_exc_info: logger.warning( "Bad PUT request on %r: %s", path, prepared_exc_info[1], exc_info=prepared_exc_info) return httputils.BAD_REQUEST if write_whole_collection: try: etag = self.Collection.create_collection( path, prepared_items, props).etag except ValueError as e: logger.warning( "Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST else: prepared_item, = prepared_items if (item and item.uid != prepared_item.uid or not item and parent_item.has_uid(prepared_item.uid)): return self.webdav_error_response( "C" if tag == "VCALENDAR" else "CR", "no-uid-conflict") href = posixpath.basename(pathutils.strip_path(path)) try: etag = parent_item.upload(href, prepared_item).etag except ValueError as e: logger.warning( "Bad PUT request on %r: %s", path, e, exc_info=True) return httputils.BAD_REQUEST headers = {"ETag": etag} return client.CREATED, headers, None