2023-02-10 22:10:47 +01:00
# This file is part of Radicale - CalDAV and CardDAV server
2018-08-28 16:19:36 +02:00
# Copyright © 2008 Nicolas Kandel
# Copyright © 2008 Pascal Halter
# Copyright © 2008-2017 Guillaume Ayoub
2024-12-15 08:29:09 +01:00
# Copyright © 2017-2020 Unrud <unrud@outlook.com>
# Copyright © 2020-2023 Tuna Celik <tuna@jakpark.com>
2025-02-10 19:33:28 +01:00
# Copyright © 2024-2025 Peter Bieringer <pb@bieringer.de>
2018-08-28 16:19:36 +02:00
#
# 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/>.
2025-02-10 19:37:19 +01:00
import errno
2018-08-28 16:19:36 +02:00
import itertools
2025-04-29 20:57:31 +02:00
import logging
2019-06-15 09:01:55 +02:00
import posixpath
2025-02-10 19:37:19 +01:00
import re
2018-08-28 16:19:36 +02:00
import socket
import sys
from http import client
2023-02-10 23:32:32 +01:00
from types import TracebackType
2023-02-10 22:10:47 +01:00
from typing import Iterator , List , Mapping , MutableMapping , Optional , Tuple
2018-08-28 16:19:36 +02:00
import vobject
2023-02-10 22:10:47 +01:00
import radicale . item as radicale_item
2024-12-15 08:37:35 +01:00
from radicale import ( httputils , pathutils , rights , storage , types , utils ,
xmlutils )
2023-02-10 22:10:47 +01:00
from radicale . app . base import Access , ApplicationBase
2020-08-17 02:14:04 +02:00
from radicale . hook import HookNotificationItem , HookNotificationItemTypes
2020-08-17 02:23:49 +02:00
from radicale . log import logger
2023-02-10 22:10:47 +01:00
MIMETYPE_TAGS : Mapping [ str , str ] = { value : key for key , value in
xmlutils . MIMETYPES . items ( ) }
2024-12-15 08:28:37 +01:00
PRODID = u " -//Radicale//NONSGML Version " + utils . package_version ( " radicale " ) + " //EN "
2023-02-10 22:10:47 +01:00
def prepare ( vobject_items : List [ vobject . base . Component ] , path : str ,
content_type : str , permission : bool , parent_permission : bool ,
tag : Optional [ str ] = None ,
write_whole_collection : Optional [ bool ] = None ) - > Tuple [
Iterator [ radicale_item . Item ] , # items
Optional [ str ] , # tag
Optional [ bool ] , # write_whole_collection
Optional [ MutableMapping [ str , str ] ] , # props
Optional [ Tuple [ type , BaseException , Optional [ TracebackType ] ] ] ] :
if ( write_whole_collection or permission and not parent_permission ) :
2020-01-17 05:00:31 +01:00
write_whole_collection = True
tag = radicale_item . predict_tag_of_whole_collection (
2020-01-17 12:45:01 +01:00
vobject_items , MIMETYPE_TAGS . get ( content_type ) )
2020-01-17 05:00:31 +01:00
if not tag :
raise ValueError ( " Can ' t determine collection tag " )
collection_path = pathutils . strip_path ( path )
2020-01-17 12:45:01 +01:00
elif ( write_whole_collection is not None and not write_whole_collection or
2023-02-10 22:10:47 +01:00
not permission and parent_permission ) :
2020-01-17 05:00:31 +01:00
write_whole_collection = False
if tag is None :
2020-01-17 12:45:01 +01:00
tag = radicale_item . predict_tag_of_parent_collection ( vobject_items )
collection_path = posixpath . dirname ( pathutils . strip_path ( path ) )
2023-02-10 22:10:47 +01:00
props : Optional [ MutableMapping [ str , str ] ] = None
2020-01-17 05:00:31 +01:00
stored_exc_info = None
items = [ ]
try :
2023-02-10 22:10:47 +01:00
if tag and write_whole_collection is not None :
2020-01-17 05:00:31 +01:00
radicale_item . check_and_sanitize_items (
2020-01-17 12:45:01 +01:00
vobject_items , is_collection = write_whole_collection , tag = tag )
2020-01-17 05:00:31 +01:00
if write_whole_collection and tag == " VCALENDAR " :
2023-02-10 22:10:47 +01:00
vobject_components : List [ vobject . base . Component ] = [ ]
2020-01-17 05:00:31 +01:00
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 (
2020-01-17 12:45:01 +01:00
sorted ( vobject_components , key = radicale_item . get_uid ) ,
2020-01-17 05:00:31 +01:00
radicale_item . get_uid )
2020-01-17 05:00:31 +01:00
for _ , components in vobject_components_by_uid :
2020-01-17 05:00:31 +01:00
vobject_collection = vobject . iCalendar ( )
for component in components :
vobject_collection . add ( component )
2024-12-15 08:28:37 +01:00
vobject_collection . add ( vobject . base . ContentLine ( " PRODID " , [ ] , PRODID ) )
2020-01-17 12:45:01 +01:00
item = radicale_item . Item ( collection_path = collection_path ,
vobject_item = vobject_collection )
2025-04-29 20:57:31 +02:00
logger . debug ( " Prepare item with UID ' %s ' " , item . uid )
try :
item . prepare ( )
except ValueError as e :
if logger . isEnabledFor ( logging . DEBUG ) :
logger . warning ( " Problem during prepare item with UID ' %s ' (content below): %s \n %s " , item . uid , e , item . _text )
else :
logger . warning ( " Problem during prepare item with UID ' %s ' (content suppressed in this loglevel): %s " , item . uid , e )
raise
2020-01-17 05:00:31 +01:00
items . append ( item )
elif write_whole_collection and tag == " VADDRESSBOOK " :
for vobject_item in vobject_items :
2020-01-17 12:45:01 +01:00
item = radicale_item . Item ( collection_path = collection_path ,
vobject_item = vobject_item )
2020-01-17 05:00:31 +01:00
item . prepare ( )
items . append ( item )
elif not write_whole_collection :
vobject_item , = vobject_items
2020-01-17 12:45:01 +01:00
item = radicale_item . Item ( collection_path = collection_path ,
vobject_item = vobject_item )
2020-01-17 05:00:31 +01:00
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
2023-02-10 22:10:47 +01:00
props = radicale_item . check_and_sanitize_props ( props )
2020-01-17 05:00:31 +01:00
except Exception :
2023-02-10 22:10:47 +01:00
exc_info_or_none_tuple = sys . exc_info ( )
assert exc_info_or_none_tuple [ 0 ] is not None
stored_exc_info = exc_info_or_none_tuple
2020-01-17 05:00:31 +01:00
2023-02-10 22:10:47 +01:00
# Use iterator for items and delete references to free memory early
def items_iter ( ) - > Iterator [ radicale_item . Item ] :
2020-01-17 05:00:31 +01:00
while items :
yield items . pop ( 0 )
2023-02-10 22:10:47 +01:00
return items_iter ( ) , tag , write_whole_collection , props , stored_exc_info
2020-01-17 05:00:31 +01:00
2023-02-10 22:10:47 +01:00
class ApplicationPartPut ( ApplicationBase ) :
2020-01-17 05:00:31 +01:00
2023-02-10 22:10:47 +01:00
def do_PUT ( self , environ : types . WSGIEnviron , base_prefix : str ,
path : str , user : str ) - > types . WSGIResponse :
2018-08-28 16:19:36 +02:00
""" Manage PUT request. """
2023-02-10 22:10:47 +01:00
access = Access ( self . _rights , user , path )
2020-04-22 19:20:07 +02:00
if not access . check ( " w " ) :
2018-08-28 16:19:36 +02:00
return httputils . NOT_ALLOWED
try :
2023-02-10 22:10:47 +01:00
content = httputils . read_request_body ( self . configuration , environ )
2018-08-28 16:19:36 +02:00
except RuntimeError as e :
2024-05-18 13:30:26 +02:00
logger . warning ( " Bad PUT request on %r (read_request_body): %s " , path , e , exc_info = True )
2018-08-28 16:19:36 +02:00
return httputils . BAD_REQUEST
2018-11-04 18:54:11 +00:00
except socket . timeout :
2023-02-10 22:10:47 +01:00
logger . debug ( " Client timed out " , exc_info = True )
2018-08-28 16:19:36 +02:00
return httputils . REQUEST_TIMEOUT
# Prepare before locking
2023-02-10 22:10:47 +01:00
content_type = environ . get ( " CONTENT_TYPE " , " " ) . split ( " ; " ,
maxsplit = 1 ) [ 0 ]
2018-08-28 16:19:36 +02:00
try :
2023-02-10 22:10:47 +01:00
vobject_items = radicale_item . read_components ( content or " " )
2018-08-28 16:19:36 +02:00
except Exception as e :
logger . warning (
2024-05-18 13:30:26 +02:00
" Bad PUT request on %r (read_components): %s " , path , e , exc_info = True )
2024-05-29 06:19:00 +02:00
if self . _log_bad_put_request_content :
2024-05-29 06:07:36 +02:00
logger . warning ( " Bad PUT request content of %r : \n %s " , path , content )
2024-06-18 08:24:25 +02:00
else :
2024-08-28 07:48:45 +02:00
logger . debug ( " Bad PUT request content: suppressed by config/option [logging] bad_put_request_content " )
2018-08-28 16:19:36 +02:00
return httputils . BAD_REQUEST
( prepared_items , prepared_tag , prepared_write_whole_collection ,
2020-01-17 05:00:31 +01:00
prepared_props , prepared_exc_info ) = prepare (
2020-04-22 19:20:07 +02:00
vobject_items , path , content_type ,
bool ( rights . intersect ( access . permissions , " Ww " ) ) ,
bool ( rights . intersect ( access . parent_permissions , " w " ) ) )
2018-08-28 16:19:36 +02:00
2025-03-27 08:30:22 +01:00
with self . _storage . acquire_lock ( " w " , user , path = path ) :
2023-02-10 22:10:47 +01:00
item = next ( iter ( self . _storage . discover ( path ) ) , None )
parent_item = next ( iter (
self . _storage . discover ( access . parent_path ) ) , None )
if not isinstance ( parent_item , storage . BaseCollection ) :
2018-08-28 16:19:36 +02:00
return httputils . CONFLICT
write_whole_collection = (
isinstance ( item , storage . BaseCollection ) or
2023-02-10 22:10:47 +01:00
not parent_item . tag )
2018-08-28 16:19:36 +02:00
if write_whole_collection :
tag = prepared_tag
else :
2023-02-10 22:10:47 +01:00
tag = parent_item . tag
2018-08-28 16:19:36 +02:00
if write_whole_collection :
2020-04-22 19:20:07 +02:00
if ( " w " if tag else " W " ) not in access . permissions :
2024-11-24 18:53:00 +01:00
if not parent_item . tag :
logger . warning ( " Not a collection (check .Radicale.props): %r " , parent_item . path )
2018-08-28 16:19:36 +02:00
return httputils . NOT_ALLOWED
2024-09-30 21:13:00 +02:00
if not self . _permit_overwrite_collection :
if ( " O " ) not in access . permissions :
2024-11-24 18:30:59 +01:00
logger . info ( " overwrite of collection is prevented by config/option [rights] permit_overwrite_collection and not explicit allowed by permssion ' O ' : %r " , path )
2024-09-30 21:13:00 +02:00
return httputils . NOT_ALLOWED
else :
if ( " o " ) in access . permissions :
2024-11-24 18:30:59 +01:00
logger . info ( " overwrite of collection is allowed by config/option [rights] permit_overwrite_collection but explicit forbidden by permission ' o ' : %r " , path )
2024-09-30 21:13:00 +02:00
return httputils . NOT_ALLOWED
2020-04-22 19:20:07 +02:00
elif " w " not in access . parent_permissions :
2018-08-28 16:19:36 +02:00
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
2024-12-24 12:04:05 +01:00
logger . warning ( " Precondition failed on PUT request for %r (HTTP_IF_MATCH: %s , item not existing) " , path , etag )
2018-08-28 16:19:36 +02:00
return httputils . PRECONDITION_FAILED
if item and etag and item . etag != etag :
# Etag asked but item not matching: item has changed
2024-12-24 12:04:05 +01:00
logger . warning ( " Precondition failed on PUT request for %r (HTTP_IF_MATCH: %s , item has different etag: %s ) " , path , etag , item . etag )
2018-08-28 16:19:36 +02:00
return httputils . PRECONDITION_FAILED
2024-12-24 12:10:47 +01:00
if item and etag :
2024-12-24 12:04:05 +01:00
logger . debug ( " Precondition passed on PUT request for %r (HTTP_IF_MATCH: %s , item has etag: %s ) " , path , etag , item . etag )
2018-08-28 16:19:36 +02:00
match = environ . get ( " HTTP_IF_NONE_MATCH " , " " ) == " * "
if item and match :
# Creation asked but item found: item can't be replaced
2024-12-24 12:04:05 +01:00
logger . warning ( " Precondition failed on PUT request for %r (HTTP_IF_NONE_MATCH: *, creation requested but item found with etag: %s ) " , path , item . etag )
2018-08-28 16:19:36 +02:00
return httputils . PRECONDITION_FAILED
2024-12-24 12:04:05 +01:00
if match :
logger . debug ( " Precondition passed on PUT request for %r (HTTP_IF_NONE_MATCH: *) " , path )
2018-08-28 16:19:36 +02:00
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 (
2020-04-22 19:20:07 +02:00
vobject_items , path , content_type ,
bool ( rights . intersect ( access . permissions , " Ww " ) ) ,
bool ( rights . intersect ( access . parent_permissions , " w " ) ) ,
tag , write_whole_collection )
2018-08-28 16:19:36 +02:00
props = prepared_props
if prepared_exc_info :
logger . warning (
2024-05-18 13:30:26 +02:00
" Bad PUT request on %r (prepare): %s " , path , prepared_exc_info [ 1 ] ,
2018-08-28 16:19:36 +02:00
exc_info = prepared_exc_info )
return httputils . BAD_REQUEST
if write_whole_collection :
try :
2020-01-14 06:19:11 +01:00
etag = self . _storage . create_collection (
2018-08-28 16:19:36 +02:00
path , prepared_items , props ) . etag
2020-08-17 02:05:02 +02:00
for item in prepared_items :
2020-08-17 02:36:22 +02:00
hook_notification_item = HookNotificationItem (
2020-08-17 03:01:21 +02:00
HookNotificationItemTypes . UPSERT ,
2020-08-17 14:43:52 +02:00
access . path ,
2020-08-17 03:01:21 +02:00
item . serialize ( )
)
2020-08-17 02:14:04 +02:00
self . _hook . notify ( hook_notification_item )
2018-08-28 16:19:36 +02:00
except ValueError as e :
logger . warning (
2024-05-18 13:30:26 +02:00
" Bad PUT request on %r (create_collection): %s " , path , e , exc_info = True )
2018-08-28 16:19:36 +02:00
return httputils . BAD_REQUEST
else :
2023-02-10 22:10:47 +01:00
assert not isinstance ( item , storage . BaseCollection )
2018-08-28 16:19:36 +02:00
prepared_item , = prepared_items
if ( item and item . uid != prepared_item . uid or
not item and parent_item . has_uid ( prepared_item . uid ) ) :
2020-05-24 13:19:30 +02:00
return self . _webdav_error_response (
client . CONFLICT , " %s :no-uid-conflict " % (
" C " if tag == " VCALENDAR " else " CR " ) )
2018-08-28 16:19:36 +02:00
2018-08-28 16:19:50 +02:00
href = posixpath . basename ( pathutils . strip_path ( path ) )
2018-08-28 16:19:36 +02:00
try :
etag = parent_item . upload ( href , prepared_item ) . etag
2020-08-17 02:36:22 +02:00
hook_notification_item = HookNotificationItem (
2020-08-17 03:01:21 +02:00
HookNotificationItemTypes . UPSERT ,
2020-08-17 14:43:52 +02:00
access . path ,
2020-08-17 03:01:21 +02:00
prepared_item . serialize ( )
)
2020-08-17 02:14:04 +02:00
self . _hook . notify ( hook_notification_item )
2018-08-28 16:19:36 +02:00
except ValueError as e :
2025-02-10 19:37:19 +01:00
# return better matching HTTP result in case errno is provided and catched
errno_match = re . search ( " \\ [Errno ([0-9]+) \\ ] " , str ( e ) )
if errno_match :
2025-02-11 16:29:33 +01:00
logger . error (
2025-02-10 19:37:19 +01:00
" Failed PUT request on %r (upload): %s " , path , e , exc_info = True )
errno_e = int ( errno_match . group ( 1 ) )
if errno_e == errno . ENOSPC :
return httputils . INSUFFICIENT_STORAGE
2025-02-11 16:17:47 +01:00
elif errno_e in [ errno . EPERM , errno . EACCES ] :
2025-02-10 19:37:19 +01:00
return httputils . FORBIDDEN
else :
return httputils . INTERNAL_SERVER_ERROR
else :
logger . warning (
" Bad PUT request on %r (upload): %s " , path , e , exc_info = True )
return httputils . BAD_REQUEST
2025-04-22 21:31:59 +02:00
if ( item and item . uid == prepared_item . uid ) :
logger . debug ( " PUT request updated existing item %r " , path )
headers = { " ETag " : etag }
return client . NO_CONTENT , headers , None
2018-08-28 16:19:36 +02:00
headers = { " ETag " : etag }
return client . CREATED , headers , None