refactor
This commit is contained in:
		
							
								
								
									
										376
									
								
								radicale/app/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										376
									
								
								radicale/app/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,376 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
| import base64 | ||||
| import datetime | ||||
| import io | ||||
| import logging | ||||
| import pkg_resources | ||||
| import posixpath | ||||
| import pprint | ||||
| import random | ||||
| import time | ||||
| import zlib | ||||
| from http import client | ||||
| from xml.etree import ElementTree as ET | ||||
|  | ||||
| from radicale import ( | ||||
|     auth, httputils, log, pathutils, rights, storage, web, xmlutils) | ||||
| from radicale.app.delete import ApplicationDeleteMixin | ||||
| from radicale.app.get import ApplicationGetMixin | ||||
| from radicale.app.head import ApplicationHeadMixin | ||||
| from radicale.app.mkcalendar import ApplicationMkcalendarMixin | ||||
| from radicale.app.mkcol import ApplicationMkcolMixin | ||||
| from radicale.app.move import ApplicationMoveMixin | ||||
| from radicale.app.options import ApplicationOptionsMixin | ||||
| from radicale.app.propfind import ApplicationPropfindMixin | ||||
| from radicale.app.proppatch import ApplicationProppatchMixin | ||||
| from radicale.app.put import ApplicationPutMixin | ||||
| from radicale.app.report import ApplicationReportMixin | ||||
| from radicale.log import logger | ||||
|  | ||||
| VERSION = pkg_resources.get_distribution("radicale").version | ||||
|  | ||||
|  | ||||
| class Application( | ||||
|         ApplicationDeleteMixin, ApplicationGetMixin, ApplicationHeadMixin, | ||||
|         ApplicationMkcalendarMixin, ApplicationMkcolMixin, | ||||
|         ApplicationMoveMixin, ApplicationOptionsMixin, | ||||
|         ApplicationPropfindMixin, ApplicationProppatchMixin, | ||||
|         ApplicationPutMixin, ApplicationReportMixin): | ||||
|  | ||||
|     """WSGI application managing collections.""" | ||||
|  | ||||
|     def __init__(self, configuration): | ||||
|         """Initialize application.""" | ||||
|         super().__init__() | ||||
|         self.configuration = configuration | ||||
|         self.Auth = auth.load(configuration) | ||||
|         self.Collection = storage.load(configuration) | ||||
|         self.Rights = rights.load(configuration) | ||||
|         self.Web = web.load(configuration) | ||||
|         self.encoding = configuration.get("encoding", "request") | ||||
|  | ||||
|     def _headers_log(self, environ): | ||||
|         """Sanitize headers for logging.""" | ||||
|         request_environ = dict(environ) | ||||
|  | ||||
|         # Mask passwords | ||||
|         mask_passwords = self.configuration.getboolean( | ||||
|             "logging", "mask_passwords") | ||||
|         authorization = request_environ.get("HTTP_AUTHORIZATION", "") | ||||
|         if mask_passwords and authorization.startswith("Basic"): | ||||
|             request_environ["HTTP_AUTHORIZATION"] = "Basic **masked**" | ||||
|         if request_environ.get("HTTP_COOKIE"): | ||||
|             request_environ["HTTP_COOKIE"] = "**masked**" | ||||
|  | ||||
|         return request_environ | ||||
|  | ||||
|     def decode(self, text, environ): | ||||
|         """Try to magically decode ``text`` according to given ``environ``.""" | ||||
|         # List of charsets to try | ||||
|         charsets = [] | ||||
|  | ||||
|         # First append content charset given in the request | ||||
|         content_type = environ.get("CONTENT_TYPE") | ||||
|         if content_type and "charset=" in content_type: | ||||
|             charsets.append( | ||||
|                 content_type.split("charset=")[1].split(";")[0].strip()) | ||||
|         # Then append default Radicale charset | ||||
|         charsets.append(self.encoding) | ||||
|         # Then append various fallbacks | ||||
|         charsets.append("utf-8") | ||||
|         charsets.append("iso8859-1") | ||||
|  | ||||
|         # Try to decode | ||||
|         for charset in charsets: | ||||
|             try: | ||||
|                 return text.decode(charset) | ||||
|             except UnicodeDecodeError: | ||||
|                 pass | ||||
|         raise UnicodeDecodeError | ||||
|  | ||||
|     def __call__(self, environ, start_response): | ||||
|         with log.register_stream(environ["wsgi.errors"]): | ||||
|             try: | ||||
|                 status, headers, answers = self._handle_request(environ) | ||||
|             except Exception as e: | ||||
|                 try: | ||||
|                     method = str(environ["REQUEST_METHOD"]) | ||||
|                 except Exception: | ||||
|                     method = "unknown" | ||||
|                 try: | ||||
|                     path = str(environ.get("PATH_INFO", "")) | ||||
|                 except Exception: | ||||
|                     path = "" | ||||
|                 logger.error("An exception occurred during %s request on %r: " | ||||
|                              "%s", method, path, e, exc_info=True) | ||||
|                 status, headers, answer = httputils.INTERNAL_SERVER_ERROR | ||||
|                 answer = answer.encode("ascii") | ||||
|                 status = "%d %s" % ( | ||||
|                     status, client.responses.get(status, "Unknown")) | ||||
|                 headers = [ | ||||
|                     ("Content-Length", str(len(answer)))] + list(headers) | ||||
|                 answers = [answer] | ||||
|             start_response(status, headers) | ||||
|         return answers | ||||
|  | ||||
|     def _handle_request(self, environ): | ||||
|         """Manage a request.""" | ||||
|         def response(status, headers=(), answer=None): | ||||
|             headers = dict(headers) | ||||
|             # Set content length | ||||
|             if answer: | ||||
|                 if hasattr(answer, "encode"): | ||||
|                     logger.debug("Response content:\n%s", answer) | ||||
|                     headers["Content-Type"] += "; charset=%s" % self.encoding | ||||
|                     answer = answer.encode(self.encoding) | ||||
|                 accept_encoding = [ | ||||
|                     encoding.strip() for encoding in | ||||
|                     environ.get("HTTP_ACCEPT_ENCODING", "").split(",") | ||||
|                     if encoding.strip()] | ||||
|  | ||||
|                 if "gzip" in accept_encoding: | ||||
|                     zcomp = zlib.compressobj(wbits=16 + zlib.MAX_WBITS) | ||||
|                     answer = zcomp.compress(answer) + zcomp.flush() | ||||
|                     headers["Content-Encoding"] = "gzip" | ||||
|  | ||||
|                 headers["Content-Length"] = str(len(answer)) | ||||
|  | ||||
|             # Add extra headers set in configuration | ||||
|             if self.configuration.has_section("headers"): | ||||
|                 for key in self.configuration.options("headers"): | ||||
|                     headers[key] = self.configuration.get("headers", key) | ||||
|  | ||||
|             # Start response | ||||
|             time_end = datetime.datetime.now() | ||||
|             status = "%d %s" % ( | ||||
|                 status, client.responses.get(status, "Unknown")) | ||||
|             logger.info( | ||||
|                 "%s response status for %r%s in %.3f seconds: %s", | ||||
|                 environ["REQUEST_METHOD"], environ.get("PATH_INFO", ""), | ||||
|                 depthinfo, (time_end - time_begin).total_seconds(), status) | ||||
|             # Return response content | ||||
|             return status, list(headers.items()), [answer] if answer else [] | ||||
|  | ||||
|         remote_host = "unknown" | ||||
|         if environ.get("REMOTE_HOST"): | ||||
|             remote_host = repr(environ["REMOTE_HOST"]) | ||||
|         elif environ.get("REMOTE_ADDR"): | ||||
|             remote_host = environ["REMOTE_ADDR"] | ||||
|         if environ.get("HTTP_X_FORWARDED_FOR"): | ||||
|             remote_host = "%r (forwarded by %s)" % ( | ||||
|                 environ["HTTP_X_FORWARDED_FOR"], remote_host) | ||||
|         remote_useragent = "" | ||||
|         if environ.get("HTTP_USER_AGENT"): | ||||
|             remote_useragent = " using %r" % environ["HTTP_USER_AGENT"] | ||||
|         depthinfo = "" | ||||
|         if environ.get("HTTP_DEPTH"): | ||||
|             depthinfo = " with depth %r" % environ["HTTP_DEPTH"] | ||||
|         time_begin = datetime.datetime.now() | ||||
|         logger.info( | ||||
|             "%s request for %r%s received from %s%s", | ||||
|             environ["REQUEST_METHOD"], environ.get("PATH_INFO", ""), depthinfo, | ||||
|             remote_host, remote_useragent) | ||||
|         headers = pprint.pformat(self._headers_log(environ)) | ||||
|         logger.debug("Request headers:\n%s", headers) | ||||
|  | ||||
|         # Let reverse proxies overwrite SCRIPT_NAME | ||||
|         if "HTTP_X_SCRIPT_NAME" in environ: | ||||
|             # script_name must be removed from PATH_INFO by the client. | ||||
|             unsafe_base_prefix = environ["HTTP_X_SCRIPT_NAME"] | ||||
|             logger.debug("Script name overwritten by client: %r", | ||||
|                          unsafe_base_prefix) | ||||
|         else: | ||||
|             # SCRIPT_NAME is already removed from PATH_INFO, according to the | ||||
|             # WSGI specification. | ||||
|             unsafe_base_prefix = environ.get("SCRIPT_NAME", "") | ||||
|         # Sanitize base prefix | ||||
|         base_prefix = pathutils.sanitize_path(unsafe_base_prefix).rstrip("/") | ||||
|         logger.debug("Sanitized script name: %r", base_prefix) | ||||
|         # Sanitize request URI (a WSGI server indicates with an empty path, | ||||
|         # that the URL targets the application root without a trailing slash) | ||||
|         path = pathutils.sanitize_path(environ.get("PATH_INFO", "")) | ||||
|         logger.debug("Sanitized path: %r", path) | ||||
|  | ||||
|         # Get function corresponding to method | ||||
|         function = getattr(self, "do_%s" % environ["REQUEST_METHOD"].upper()) | ||||
|  | ||||
|         # If "/.well-known" is not available, clients query "/" | ||||
|         if path == "/.well-known" or path.startswith("/.well-known/"): | ||||
|             return response(*httputils.NOT_FOUND) | ||||
|  | ||||
|         # Ask authentication backend to check rights | ||||
|         login = password = "" | ||||
|         external_login = self.Auth.get_external_login(environ) | ||||
|         authorization = environ.get("HTTP_AUTHORIZATION", "") | ||||
|         if external_login: | ||||
|             login, password = external_login | ||||
|             login, password = login or "", password or "" | ||||
|         elif authorization.startswith("Basic"): | ||||
|             authorization = authorization[len("Basic"):].strip() | ||||
|             login, password = self.decode(base64.b64decode( | ||||
|                 authorization.encode("ascii")), environ).split(":", 1) | ||||
|  | ||||
|         user = self.Auth.login(login, password) or "" if login else "" | ||||
|         if user and login == user: | ||||
|             logger.info("Successful login: %r", user) | ||||
|         elif user: | ||||
|             logger.info("Successful login: %r -> %r", login, user) | ||||
|         elif login: | ||||
|             logger.info("Failed login attempt: %r", login) | ||||
|             # Random delay to avoid timing oracles and bruteforce attacks | ||||
|             delay = self.configuration.getfloat("auth", "delay") | ||||
|             if delay > 0: | ||||
|                 random_delay = delay * (0.5 + random.random()) | ||||
|                 logger.debug("Sleeping %.3f seconds", random_delay) | ||||
|                 time.sleep(random_delay) | ||||
|  | ||||
|         if user and not pathutils.is_safe_path_component(user): | ||||
|             # Prevent usernames like "user/calendar.ics" | ||||
|             logger.info("Refused unsafe username: %r", user) | ||||
|             user = "" | ||||
|  | ||||
|         # Create principal collection | ||||
|         if user: | ||||
|             principal_path = "/%s/" % user | ||||
|             if self.Rights.authorized(user, principal_path, "W"): | ||||
|                 with self.Collection.acquire_lock("r", user): | ||||
|                     principal = next( | ||||
|                         self.Collection.discover(principal_path, depth="1"), | ||||
|                         None) | ||||
|                 if not principal: | ||||
|                     with self.Collection.acquire_lock("w", user): | ||||
|                         try: | ||||
|                             self.Collection.create_collection(principal_path) | ||||
|                         except ValueError as e: | ||||
|                             logger.warning("Failed to create principal " | ||||
|                                            "collection %r: %s", user, e) | ||||
|                             user = "" | ||||
|             else: | ||||
|                 logger.warning("Access to principal path %r denied by " | ||||
|                                "rights backend", principal_path) | ||||
|  | ||||
|         if self.configuration.getboolean("internal", "internal_server"): | ||||
|             # Verify content length | ||||
|             content_length = int(environ.get("CONTENT_LENGTH") or 0) | ||||
|             if content_length: | ||||
|                 max_content_length = self.configuration.getint( | ||||
|                     "server", "max_content_length") | ||||
|                 if max_content_length and content_length > max_content_length: | ||||
|                     logger.info("Request body too large: %d", content_length) | ||||
|                     return response(*httputils.REQUEST_ENTITY_TOO_LARGE) | ||||
|  | ||||
|         if not login or user: | ||||
|             status, headers, answer = function( | ||||
|                 environ, base_prefix, path, user) | ||||
|             if (status, headers, answer) == httputils.NOT_ALLOWED: | ||||
|                 logger.info("Access to %r denied for %s", path, | ||||
|                             repr(user) if user else "anonymous user") | ||||
|         else: | ||||
|             status, headers, answer = httputils.NOT_ALLOWED | ||||
|  | ||||
|         if ((status, headers, answer) == httputils.NOT_ALLOWED and not user and | ||||
|                 not external_login): | ||||
|             # Unknown or unauthorized user | ||||
|             logger.debug("Asking client for authentication") | ||||
|             status = client.UNAUTHORIZED | ||||
|             realm = self.configuration.get("auth", "realm") | ||||
|             headers = dict(headers) | ||||
|             headers.update({ | ||||
|                 "WWW-Authenticate": | ||||
|                 "Basic realm=\"%s\"" % realm}) | ||||
|  | ||||
|         return response(status, headers, answer) | ||||
|  | ||||
|     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.sanitize_path( | ||||
|                 "/%s/" % posixpath.dirname(path.strip("/"))) | ||||
|             if self.Rights.authorized(user, parent_path, parent_permissions): | ||||
|                 return True | ||||
|         return False | ||||
|  | ||||
|     def read_raw_content(self, environ): | ||||
|         content_length = int(environ.get("CONTENT_LENGTH") or 0) | ||||
|         if not content_length: | ||||
|             return b"" | ||||
|         content = environ["wsgi.input"].read(content_length) | ||||
|         if len(content) < content_length: | ||||
|             raise RuntimeError("Request body too short: %d" % len(content)) | ||||
|         return content | ||||
|  | ||||
|     def read_content(self, environ): | ||||
|         content = self.decode(self.read_raw_content(environ), environ) | ||||
|         logger.debug("Request content:\n%s", content) | ||||
|         return content | ||||
|  | ||||
|     def read_xml_content(self, environ): | ||||
|         content = self.decode(self.read_raw_content(environ), environ) | ||||
|         if not content: | ||||
|             return None | ||||
|         try: | ||||
|             xml_content = ET.fromstring(content) | ||||
|         except ET.ParseError as e: | ||||
|             logger.debug("Request content (Invalid XML):\n%s", content) | ||||
|             raise RuntimeError("Failed to parse XML: %s" % e) from e | ||||
|         if logger.isEnabledFor(logging.DEBUG): | ||||
|             logger.debug("Request content:\n%s", | ||||
|                          xmlutils.pretty_xml(xml_content)) | ||||
|         return xml_content | ||||
|  | ||||
|     def write_xml_content(self, xml_content): | ||||
|         if logger.isEnabledFor(logging.DEBUG): | ||||
|             logger.debug("Response content:\n%s", | ||||
|                          xmlutils.pretty_xml(xml_content)) | ||||
|         f = io.BytesIO() | ||||
|         ET.ElementTree(xml_content).write(f, encoding=self.encoding, | ||||
|                                           xml_declaration=True) | ||||
|         return f.getvalue() | ||||
|  | ||||
|     def webdav_error_response(self, namespace, name, | ||||
|                               status=httputils.WEBDAV_PRECONDITION_FAILED[0]): | ||||
|         """Generate XML error response.""" | ||||
|         headers = {"Content-Type": "text/xml; charset=%s" % self.encoding} | ||||
|         content = self.write_xml_content( | ||||
|             xmlutils.webdav_error(namespace, name)) | ||||
|         return status, headers, content | ||||
							
								
								
									
										70
									
								
								radicale/app/delete.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								radicale/app/delete.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| from http import client | ||||
| from xml.etree import ElementTree as ET | ||||
|  | ||||
| from radicale import httputils, storage, xmlutils | ||||
|  | ||||
|  | ||||
| def xml_delete(base_prefix, path, collection, href=None): | ||||
|     """Read and answer DELETE requests. | ||||
|  | ||||
|     Read rfc4918-9.6 for info. | ||||
|  | ||||
|     """ | ||||
|     collection.delete(href) | ||||
|  | ||||
|     multistatus = ET.Element(xmlutils.make_tag("D", "multistatus")) | ||||
|     response = ET.Element(xmlutils.make_tag("D", "response")) | ||||
|     multistatus.append(response) | ||||
|  | ||||
|     href = ET.Element(xmlutils.make_tag("D", "href")) | ||||
|     href.text = xmlutils.make_href(base_prefix, path) | ||||
|     response.append(href) | ||||
|  | ||||
|     status = ET.Element(xmlutils.make_tag("D", "status")) | ||||
|     status.text = xmlutils.make_response(200) | ||||
|     response.append(status) | ||||
|  | ||||
|     return multistatus | ||||
|  | ||||
|  | ||||
| class ApplicationDeleteMixin: | ||||
|     def do_DELETE(self, environ, base_prefix, path, user): | ||||
|         """Manage DELETE request.""" | ||||
|         if not self.access(user, 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): | ||||
|                 return httputils.NOT_ALLOWED | ||||
|             if_match = environ.get("HTTP_IF_MATCH", "*") | ||||
|             if if_match not in ("*", item.etag): | ||||
|                 # ETag precondition not verified, do not delete item | ||||
|                 return httputils.PRECONDITION_FAILED | ||||
|             if isinstance(item, storage.BaseCollection): | ||||
|                 xml_answer = xml_delete(base_prefix, path, item) | ||||
|             else: | ||||
|                 xml_answer = xml_delete( | ||||
|                     base_prefix, path, item.collection, item.href) | ||||
|             headers = {"Content-Type": "text/xml; charset=%s" % self.encoding} | ||||
|             return client.OK, headers, self.write_xml_content(xml_answer) | ||||
							
								
								
									
										105
									
								
								radicale/app/get.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								radicale/app/get.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
| import posixpath | ||||
| from http import client | ||||
| from urllib.parse import quote | ||||
|  | ||||
| from radicale import httputils, storage, xmlutils | ||||
| from radicale.log import logger | ||||
|  | ||||
|  | ||||
| def propose_filename(collection): | ||||
|     """Propose a filename for a collection.""" | ||||
|     tag = collection.get_meta("tag") | ||||
|     if tag == "VADDRESSBOOK": | ||||
|         fallback_title = "Address book" | ||||
|         suffix = ".vcf" | ||||
|     elif tag == "VCALENDAR": | ||||
|         fallback_title = "Calendar" | ||||
|         suffix = ".ics" | ||||
|     else: | ||||
|         fallback_title = posixpath.basename(collection.path) | ||||
|         suffix = "" | ||||
|     title = collection.get_meta("D:displayname") or fallback_title | ||||
|     if title and not title.lower().endswith(suffix.lower()): | ||||
|         title += suffix | ||||
|     return title | ||||
|  | ||||
|  | ||||
| class ApplicationGetMixin: | ||||
|     def _content_disposition_attachement(self, filename): | ||||
|         value = "attachement" | ||||
|         try: | ||||
|             encoded_filename = quote(filename, encoding=self.encoding) | ||||
|         except UnicodeEncodeError as e: | ||||
|             logger.warning("Failed to encode filename: %r", filename, | ||||
|                            exc_info=True) | ||||
|             encoded_filename = "" | ||||
|         if encoded_filename: | ||||
|             value += "; filename*=%s''%s" % (self.encoding, encoded_filename) | ||||
|         return value | ||||
|  | ||||
|     def do_GET(self, environ, base_prefix, path, user): | ||||
|         """Manage GET request.""" | ||||
|         # Redirect to .web if the root URL is requested | ||||
|         if not path.strip("/"): | ||||
|             web_path = ".web" | ||||
|             if not environ.get("PATH_INFO"): | ||||
|                 web_path = posixpath.join(posixpath.basename(base_prefix), | ||||
|                                           web_path) | ||||
|             return (client.FOUND, | ||||
|                     {"Location": web_path, "Content-Type": "text/plain"}, | ||||
|                     "Redirected to %s" % web_path) | ||||
|         # Dispatch .web URL to web module | ||||
|         if path == "/.web" or path.startswith("/.web/"): | ||||
|             return self.Web.get(environ, base_prefix, path, user) | ||||
|         if not self.access(user, path, "r"): | ||||
|             return httputils.NOT_ALLOWED | ||||
|         with self.Collection.acquire_lock("r", user): | ||||
|             item = next(self.Collection.discover(path), None) | ||||
|             if not item: | ||||
|                 return httputils.NOT_FOUND | ||||
|             if not self.access(user, path, "r", item): | ||||
|                 return httputils.NOT_ALLOWED | ||||
|             if isinstance(item, storage.BaseCollection): | ||||
|                 tag = item.get_meta("tag") | ||||
|                 if not tag: | ||||
|                     return httputils.DIRECTORY_LISTING | ||||
|                 content_type = xmlutils.MIMETYPES[tag] | ||||
|                 content_disposition = self._content_disposition_attachement( | ||||
|                     propose_filename(item)) | ||||
|             else: | ||||
|                 content_type = xmlutils.OBJECT_MIMETYPES[item.name] | ||||
|                 content_disposition = "" | ||||
|             headers = { | ||||
|                 "Content-Type": content_type, | ||||
|                 "Last-Modified": item.last_modified, | ||||
|                 "ETag": item.etag} | ||||
|             if content_disposition: | ||||
|                 headers["Content-Disposition"] = content_disposition | ||||
|             answer = item.serialize() | ||||
|             return client.OK, headers, answer | ||||
							
								
								
									
										33
									
								
								radicale/app/head.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								radicale/app/head.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
|  | ||||
| class ApplicationHeadMixin: | ||||
|     def do_HEAD(self, environ, base_prefix, path, user): | ||||
|         """Manage HEAD request.""" | ||||
|         status, headers, answer = self.do_GET( | ||||
|             environ, base_prefix, path, user) | ||||
|         return status, headers, None | ||||
							
								
								
									
										80
									
								
								radicale/app/mkcalendar.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								radicale/app/mkcalendar.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
| import posixpath | ||||
| import socket | ||||
| from http import client | ||||
|  | ||||
| from radicale import httputils | ||||
| from radicale import item as radicale_item | ||||
| from radicale import pathutils, storage, xmlutils | ||||
| from radicale.log import logger | ||||
|  | ||||
|  | ||||
| class ApplicationMkcalendarMixin: | ||||
|     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 as e: | ||||
|             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.sanitize_path( | ||||
|                 "/%s/" % posixpath.dirname(path.strip("/"))) | ||||
|             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 | ||||
							
								
								
									
										81
									
								
								radicale/app/mkcol.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								radicale/app/mkcol.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
| import posixpath | ||||
| import socket | ||||
| from http import client | ||||
|  | ||||
| from radicale import httputils | ||||
| from radicale import item as radicale_item | ||||
| from radicale import pathutils, storage, xmlutils | ||||
| from radicale.log import logger | ||||
|  | ||||
|  | ||||
| class ApplicationMkcolMixin: | ||||
|     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 as e: | ||||
|             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.sanitize_path( | ||||
|                 "/%s/" % posixpath.dirname(path.strip("/"))) | ||||
|             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 | ||||
							
								
								
									
										93
									
								
								radicale/app/move.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								radicale/app/move.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
| import posixpath | ||||
| from http import client | ||||
| from urllib.parse import urlparse | ||||
|  | ||||
| from radicale import httputils, pathutils, storage | ||||
| from radicale.log import logger | ||||
|  | ||||
|  | ||||
| class ApplicationMoveMixin: | ||||
|     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.sanitize_path( | ||||
|                 "/%s/" % posixpath.dirname(to_path.strip("/"))) | ||||
|             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(to_path.strip("/")) | ||||
|             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 | ||||
							
								
								
									
										39
									
								
								radicale/app/options.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								radicale/app/options.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
| from http import client | ||||
|  | ||||
| from radicale import httputils | ||||
|  | ||||
|  | ||||
| class ApplicationOptionsMixin: | ||||
|     def do_OPTIONS(self, environ, base_prefix, path, user): | ||||
|         """Manage OPTIONS request.""" | ||||
|         headers = { | ||||
|             "Allow": ", ".join( | ||||
|                 name[3:] for name in dir(self) if name.startswith("do_")), | ||||
|             "DAV": httputils.DAV_HEADERS} | ||||
|         return client.OK, headers, None | ||||
							
								
								
									
										395
									
								
								radicale/app/propfind.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										395
									
								
								radicale/app/propfind.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,395 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
| import itertools | ||||
| import posixpath | ||||
| import socket | ||||
| from http import client | ||||
| from xml.etree import ElementTree as ET | ||||
|  | ||||
| from radicale import httputils, pathutils, rights, storage, xmlutils | ||||
| from radicale.log import logger | ||||
|  | ||||
|  | ||||
| def xml_propfind(base_prefix, path, xml_request, allowed_items, user): | ||||
|     """Read and answer PROPFIND requests. | ||||
|  | ||||
|     Read rfc4918-9.1 for info. | ||||
|  | ||||
|     The collections parameter is a list of collections that are to be included | ||||
|     in the output. | ||||
|  | ||||
|     """ | ||||
|     # A client may choose not to submit a request body.  An empty PROPFIND | ||||
|     # request body MUST be treated as if it were an 'allprop' request. | ||||
|     top_tag = (xml_request[0] if xml_request is not None else | ||||
|                ET.Element(xmlutils.make_tag("D", "allprop"))) | ||||
|  | ||||
|     props = () | ||||
|     allprop = False | ||||
|     propname = False | ||||
|     if top_tag.tag == xmlutils.make_tag("D", "allprop"): | ||||
|         allprop = True | ||||
|     elif top_tag.tag == xmlutils.make_tag("D", "propname"): | ||||
|         propname = True | ||||
|     elif top_tag.tag == xmlutils.make_tag("D", "prop"): | ||||
|         props = [prop.tag for prop in top_tag] | ||||
|  | ||||
|     if xmlutils.make_tag("D", "current-user-principal") in props and not user: | ||||
|         # Ask for authentication | ||||
|         # Returning the DAV:unauthenticated pseudo-principal as specified in | ||||
|         # RFC 5397 doesn't seem to work with DAVdroid. | ||||
|         return client.FORBIDDEN, None | ||||
|  | ||||
|     # Writing answer | ||||
|     multistatus = ET.Element(xmlutils.make_tag("D", "multistatus")) | ||||
|  | ||||
|     for item, permission in allowed_items: | ||||
|         write = permission == "w" | ||||
|         response = xml_propfind_response( | ||||
|             base_prefix, path, item, props, user, write=write, | ||||
|             allprop=allprop, propname=propname) | ||||
|         if response: | ||||
|             multistatus.append(response) | ||||
|  | ||||
|     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 = "/%s/" % item.path if item.path else "/" | ||||
|     else: | ||||
|         uri = "/" + 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 | ||||
|  | ||||
|  | ||||
| class ApplicationPropfindMixin: | ||||
|     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.sanitize_path("/%s/" % item.path) | ||||
|                 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.sanitize_path("/%s/" % item.collection.path) | ||||
|                 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 do_PROPFIND(self, environ, base_prefix, path, user): | ||||
|         """Manage PROPFIND request.""" | ||||
|         if not self.access(user, path, "r"): | ||||
|             return httputils.NOT_ALLOWED | ||||
|         try: | ||||
|             xml_content = self.read_xml_content(environ) | ||||
|         except RuntimeError as e: | ||||
|             logger.warning( | ||||
|                 "Bad PROPFIND request on %r: %s", path, e, exc_info=True) | ||||
|             return httputils.BAD_REQUEST | ||||
|         except socket.timeout as e: | ||||
|             logger.debug("client timed out", exc_info=True) | ||||
|             return httputils.REQUEST_TIMEOUT | ||||
|         with self.Collection.acquire_lock("r", user): | ||||
|             items = self.Collection.discover( | ||||
|                 path, environ.get("HTTP_DEPTH", "0")) | ||||
|             # take root item for rights checking | ||||
|             item = next(items, None) | ||||
|             if not item: | ||||
|                 return httputils.NOT_FOUND | ||||
|             if not self.access(user, path, "r", item): | ||||
|                 return httputils.NOT_ALLOWED | ||||
|             # put item back | ||||
|             items = itertools.chain([item], items) | ||||
|             allowed_items = self._collect_allowed_items(items, user) | ||||
|             headers = {"DAV": httputils.DAV_HEADERS, | ||||
|                        "Content-Type": "text/xml; charset=%s" % self.encoding} | ||||
|             status, xml_answer = xml_propfind( | ||||
|                 base_prefix, path, xml_content, allowed_items, user) | ||||
|             if status == client.FORBIDDEN: | ||||
|                 return httputils.NOT_ALLOWED | ||||
|             return status, headers, self.write_xml_content(xml_answer) | ||||
							
								
								
									
										126
									
								
								radicale/app/proppatch.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								radicale/app/proppatch.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
| import socket | ||||
| from http import client | ||||
| from xml.etree import ElementTree as ET | ||||
|  | ||||
| from radicale import httputils | ||||
| from radicale import item as radicale_item | ||||
| from radicale import storage, xmlutils | ||||
| from radicale.log import logger | ||||
|  | ||||
|  | ||||
| def xml_add_propstat_to(element, tag, status_number): | ||||
|     """Add a PROPSTAT response structure to an 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_tag("D", "propstat")) | ||||
|     element.append(propstat) | ||||
|  | ||||
|     prop = ET.Element(xmlutils.make_tag("D", "prop")) | ||||
|     propstat.append(prop) | ||||
|  | ||||
|     clark_tag = tag if "{" in tag else xmlutils.make_tag(*tag.split(":", 1)) | ||||
|     prop_tag = ET.Element(clark_tag) | ||||
|     prop.append(prop_tag) | ||||
|  | ||||
|     status = ET.Element(xmlutils.make_tag("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 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_tag("D", "multistatus")) | ||||
|     response = ET.Element(xmlutils.make_tag("D", "response")) | ||||
|     multistatus.append(response) | ||||
|  | ||||
|     href = ET.Element(xmlutils.make_tag("D", "href")) | ||||
|     href.text = xmlutils.make_href(base_prefix, path) | ||||
|     response.append(href) | ||||
|  | ||||
|     new_props = collection.get_meta() | ||||
|     for short_name, value in props_to_set.items(): | ||||
|         new_props[short_name] = value | ||||
|         xml_add_propstat_to(response, short_name, 200) | ||||
|     for short_name in props_to_remove: | ||||
|         try: | ||||
|             del new_props[short_name] | ||||
|         except KeyError: | ||||
|             pass | ||||
|         xml_add_propstat_to(response, short_name, 200) | ||||
|     radicale_item.check_and_sanitize_props(new_props) | ||||
|     collection.set_meta(new_props) | ||||
|  | ||||
|     return multistatus | ||||
|  | ||||
|  | ||||
| class ApplicationProppatchMixin: | ||||
|     def do_PROPPATCH(self, environ, base_prefix, path, user): | ||||
|         """Manage PROPPATCH request.""" | ||||
|         if not self.access(user, path, "w"): | ||||
|             return httputils.NOT_ALLOWED | ||||
|         try: | ||||
|             xml_content = self.read_xml_content(environ) | ||||
|         except RuntimeError as e: | ||||
|             logger.warning( | ||||
|                 "Bad PROPPATCH request on %r: %s", path, e, exc_info=True) | ||||
|             return httputils.BAD_REQUEST | ||||
|         except socket.timeout as e: | ||||
|             logger.debug("client timed out", exc_info=True) | ||||
|             return httputils.REQUEST_TIMEOUT | ||||
|         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): | ||||
|                 return httputils.NOT_ALLOWED | ||||
|             if not isinstance(item, storage.BaseCollection): | ||||
|                 return httputils.FORBIDDEN | ||||
|             headers = {"DAV": httputils.DAV_HEADERS, | ||||
|                        "Content-Type": "text/xml; charset=%s" % self.encoding} | ||||
|             try: | ||||
|                 xml_answer = xml_proppatch(base_prefix, path, xml_content, | ||||
|                                            item) | ||||
|             except ValueError as e: | ||||
|                 logger.warning( | ||||
|                     "Bad PROPPATCH request on %r: %s", path, e, exc_info=True) | ||||
|                 return httputils.BAD_REQUEST | ||||
|             return (client.MULTI_STATUS, headers, | ||||
|                     self.write_xml_content(xml_answer)) | ||||
							
								
								
									
										230
									
								
								radicale/app/put.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								radicale/app/put.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,230 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
| import itertools | ||||
| import posixpath | ||||
| import socket | ||||
| import sys | ||||
| from http import client | ||||
|  | ||||
| import vobject | ||||
|  | ||||
| from radicale import httputils | ||||
| from radicale import item as radicale_item | ||||
| from radicale import pathutils, storage, xmlutils | ||||
| from radicale.log import logger | ||||
|  | ||||
|  | ||||
| class ApplicationPutMixin: | ||||
|     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 as e: | ||||
|             logger.debug("client timed out", exc_info=True) | ||||
|             return httputils.REQUEST_TIMEOUT | ||||
|         # Prepare before locking | ||||
|         parent_path = pathutils.sanitize_path( | ||||
|             "/%s/" % posixpath.dirname(path.strip("/"))) | ||||
|         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.sanitize_path(path).strip("/") | ||||
|             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 = storage.predict_tag_of_parent_collection( | ||||
|                         vobject_items) | ||||
|                 collection_path = posixpath.dirname( | ||||
|                     pathutils.sanitize_path(path).strip("/")) | ||||
|             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(path.strip("/")) | ||||
|                 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 | ||||
							
								
								
									
										296
									
								
								radicale/app/report.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								radicale/app/report.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,296 @@ | ||||
| # This file is part of Radicale Server - Calendar Server | ||||
| # Copyright © 2008 Nicolas Kandel | ||||
| # Copyright © 2008 Pascal Halter | ||||
| # Copyright © 2008-2017 Guillaume Ayoub | ||||
| # Copyright © 2017-2018 Unrud <unrud@outlook.com> | ||||
| # | ||||
| # This library is free software: you can redistribute it and/or modify | ||||
| # it under the terms of the GNU General Public License as published by | ||||
| # the Free Software Foundation, either version 3 of the License, or | ||||
| # (at your option) any later version. | ||||
| # | ||||
| # This library is distributed in the hope that it will be useful, | ||||
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| # GNU General Public License for more details. | ||||
| # | ||||
| # You should have received a copy of the GNU General Public License | ||||
| # along with Radicale.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| """ | ||||
| Radicale WSGI application. | ||||
|  | ||||
| Can be used with an external WSGI server or the built-in server. | ||||
|  | ||||
| """ | ||||
|  | ||||
| import contextlib | ||||
| import posixpath | ||||
| import socket | ||||
| from http import client | ||||
| from urllib.parse import unquote, urlparse | ||||
| from xml.etree import ElementTree as ET | ||||
|  | ||||
| from radicale import httputils, pathutils, storage, xmlutils | ||||
| from radicale.item import filter as radicale_filter | ||||
| from radicale.log import logger | ||||
|  | ||||
|  | ||||
| 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 = ("/" + 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 = "/" + 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_all_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 = "/" + 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_item_response(base_prefix, href, found_props=(), not_found_props=(), | ||||
|                       found_item=True): | ||||
|     response = ET.Element(xmlutils.make_tag("D", "response")) | ||||
|  | ||||
|     href_tag = ET.Element(xmlutils.make_tag("D", "href")) | ||||
|     href_tag.text = xmlutils.make_href(base_prefix, href) | ||||
|     response.append(href_tag) | ||||
|  | ||||
|     if found_item: | ||||
|         for code, props in ((200, found_props), (404, not_found_props)): | ||||
|             if props: | ||||
|                 propstat = ET.Element(xmlutils.make_tag("D", "propstat")) | ||||
|                 status = ET.Element(xmlutils.make_tag("D", "status")) | ||||
|                 status.text = xmlutils.make_response(code) | ||||
|                 prop_tag = ET.Element(xmlutils.make_tag("D", "prop")) | ||||
|                 for prop in props: | ||||
|                     prop_tag.append(prop) | ||||
|                 propstat.append(prop_tag) | ||||
|                 propstat.append(status) | ||||
|                 response.append(propstat) | ||||
|     else: | ||||
|         status = ET.Element(xmlutils.make_tag("D", "status")) | ||||
|         status.text = xmlutils.make_response(404) | ||||
|         response.append(status) | ||||
|  | ||||
|     return response | ||||
|  | ||||
|  | ||||
| class ApplicationReportMixin: | ||||
|     def do_REPORT(self, environ, base_prefix, path, user): | ||||
|         """Manage REPORT request.""" | ||||
|         if not self.access(user, path, "r"): | ||||
|             return httputils.NOT_ALLOWED | ||||
|         try: | ||||
|             xml_content = self.read_xml_content(environ) | ||||
|         except RuntimeError as e: | ||||
|             logger.warning( | ||||
|                 "Bad REPORT request on %r: %s", path, e, exc_info=True) | ||||
|             return httputils.BAD_REQUEST | ||||
|         except socket.timeout as e: | ||||
|             logger.debug("client timed out", exc_info=True) | ||||
|             return httputils.REQUEST_TIMEOUT | ||||
|         with contextlib.ExitStack() as lock_stack: | ||||
|             lock_stack.enter_context(self.Collection.acquire_lock("r", user)) | ||||
|             item = next(self.Collection.discover(path), None) | ||||
|             if not item: | ||||
|                 return httputils.NOT_FOUND | ||||
|             if not self.access(user, path, "r", item): | ||||
|                 return httputils.NOT_ALLOWED | ||||
|             if isinstance(item, storage.BaseCollection): | ||||
|                 collection = item | ||||
|             else: | ||||
|                 collection = item.collection | ||||
|             headers = {"Content-Type": "text/xml; charset=%s" % self.encoding} | ||||
|             try: | ||||
|                 status, xml_answer = xml_report( | ||||
|                     base_prefix, path, xml_content, collection, | ||||
|                     lock_stack.close) | ||||
|             except ValueError as e: | ||||
|                 logger.warning( | ||||
|                     "Bad REPORT request on %r: %s", path, e, exc_info=True) | ||||
|                 return httputils.BAD_REQUEST | ||||
|             return (status, headers, self.write_xml_content(xml_answer)) | ||||
		Reference in New Issue
	
	Block a user
	 Unrud
					Unrud