3333
3434INTERNAL_TYPES : Sequence [str ] = ("csv" , "files" , "none" )
3535
36- DB_FIELDS_V1 : Sequence [str ] = ('ShareType' , 'PathOrToken' , 'PathMapped' , 'Owner' , 'User' , 'Permissions' , 'EnabledByOwner' , 'EnabledByUser' , 'HiddenByOwner' , 'HiddenByUser' , 'TimestampCreated' , 'TimestampUpdated' )
36+ DB_FIELDS_V1 : Sequence [str ] = ('ShareType' , 'PathOrToken' , 'PathMapped' , 'Owner' , 'User' , 'Permissions' , 'EnabledByOwner' , 'EnabledByUser' , 'HiddenByOwner' , 'HiddenByUser' , 'TimestampCreated' , 'TimestampUpdated' , 'Properties' )
3737DB_FIELDS_V1_BOOL : Sequence [str ] = ('EnabledByOwner' , 'EnabledByUser' , 'HiddenByOwner' , 'HiddenByUser' )
3838DB_FIELDS_V1_INT : Sequence [str ] = ('TimestampCreated' , 'TimestampUpdated' )
3939# ShareType: <token|map>
7676
7777USER_PATTERN : str = "([a-zA-Z0-9@]+)" # TODO: extend or find better source
7878
79+ OVERLAY_PROPERTIES_WHITELIST : Sequence [str ] = ("C:calendar-description" , "ICAL:calendar-color" , "CR:addressbook-description" , "INF:addressbook-color" )
80+
7981
8082def load (configuration : "config.Configuration" ) -> "BaseSharing" :
8183 """Load the sharing database module chosen in configuration."""
@@ -178,20 +180,22 @@ def create_sharing(self,
178180 Permissions : str = "r" ,
179181 EnabledByOwner : bool = False , EnabledByUser : bool = False ,
180182 HiddenByOwner : bool = True , HiddenByUser : bool = True ,
181- Timestamp : int = 0 ) -> dict :
183+ Timestamp : int = 0 ,
184+ Properties : Union [dict , None ] = None ) -> dict :
182185 """ create sharing """
183186 return {"status" : "not-implemented" }
184187
185188 def update_sharing (self ,
186189 ShareType : str ,
187190 PathOrToken : str ,
188- Owner : Union [ str , None ] = None ,
191+ OwnerOrUser : str ,
189192 User : Union [str , None ] = None ,
190193 PathMapped : Union [str , None ] = None ,
191194 Permissions : Union [str , None ] = None ,
192195 EnabledByOwner : Union [bool , None ] = None ,
193196 HiddenByOwner : Union [bool , None ] = None ,
194- Timestamp : int = 0 ) -> dict :
197+ Timestamp : int = 0 ,
198+ Properties : Union [dict , None ] = None ) -> dict :
195199 """ update sharing """
196200 return {"status" : "not-implemented" }
197201
@@ -391,6 +395,8 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
391395 PathOrToken: <path> (mandatory)
392396 User: <target_user> (mandatory)
393397
398+ action: (token|map)/update
399+
394400 action: (token|map)/(delete|disable|enable|hide|unhide)
395401 PathOrToken: <path|token> (mandatory)
396402
@@ -408,7 +414,7 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
408414 Status in JSON/TEXT (TEXT can be parsed by shell)
409415
410416 """
411- if not self .sharing_collection_by_map and not self . sharing_collection_by_token :
417+ if not self ._enabled :
412418 # API is not enabled
413419 return httputils .NOT_FOUND
414420
@@ -420,7 +426,7 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
420426 if not path .startswith ("/.sharing/v1/" ):
421427 return httputils .NOT_FOUND
422428
423- # split into ShareType and action or "info"
429+ # split into ShareType and action
424430 ShareType_action = path .removeprefix ("/.sharing/v1/" )
425431 match = re .search ('([a-z]+)/([a-z]+)$' , ShareType_action )
426432 if not match :
@@ -481,7 +487,21 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
481487 # convert arrays into single value
482488 request_data = {}
483489 for key in request_parsed :
484- request_data [key ] = request_parsed [key ][0 ]
490+ if key == "Properties" :
491+ # Properties key value parser
492+ properties_dict : dict = {}
493+ for entry in request_parsed [key ]:
494+ m = re .search ('^([^=]+)=([^=]+)$' , entry )
495+ if not m :
496+ return httputils .bad_request ("Invalid properties format in form" )
497+ token = m .group (1 ).lstrip ('"\' ' ).rstrip ('"\' ' )
498+ value = m .group (2 ).lstrip ('"\' ' ).rstrip ('"\' ' )
499+ properties_dict [token ] = value
500+ if logger .isEnabledFor (logging .DEBUG ):
501+ logger .debug ("TRACE/sharing/API: converted Properties from form into dict: %r" , properties_dict )
502+ request_data [key ] = properties_dict
503+ else :
504+ request_data [key ] = request_parsed [key ][0 ]
485505 if logger .isEnabledFor (logging .DEBUG ):
486506 logger .debug ("TRACE/" + api_info + " (form): %r" , f"{ request_data } " )
487507 else :
@@ -518,6 +538,7 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
518538 HiddenByOwner : Union [bool , None ] = None
519539 EnabledByUser : Union [bool , None ] = None
520540 HiddenByUser : Union [bool , None ] = None
541+ Properties : Union [dict , None ] = None
521542
522543 # parameters sanity check
523544 for key in request_data :
@@ -552,12 +573,9 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
552573
553574 # check for mandatory parameters
554575 if 'PathMapped' not in request_data :
555- if action == 'info' :
576+ if action in [ 'info' , 'list' , 'update' ] :
556577 # ignored
557578 pass
558- elif action == "list" :
559- # optional
560- pass
561579 else :
562580 if ShareType == "token" and action != 'create' :
563581 # optional
@@ -588,6 +606,13 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
588606 if 'Permissions' in request_data :
589607 Permissions = request_data ['Permissions' ]
590608
609+ if 'Properties' in request_data :
610+ # verify against whitelist
611+ for entry in request_data ['Properties' ]:
612+ if entry not in OVERLAY_PROPERTIES_WHITELIST :
613+ return httputils .bad_request ("Property not supported to overlay: %r" % entry )
614+ Properties = request_data ['Properties' ]
615+
591616 if ShareType == "map" :
592617 if action == 'info' :
593618 # ignored
@@ -609,6 +634,11 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
609634 answer ['ApiVersion' ] = 1
610635 Timestamp = int ((datetime .now () - datetime (1970 , 1 , 1 )).total_seconds ())
611636
637+ if not self .sharing_collection_by_map and not self .sharing_collection_by_token :
638+ if not action == 'info' :
639+ # API is not enabled
640+ return httputils .NOT_FOUND
641+
612642 # action: list
613643 if action == "list" :
614644 if logger .isEnabledFor (logging .DEBUG ):
@@ -691,7 +721,8 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
691721 Owner = Owner , User = Owner ,
692722 Permissions = str (Permissions ), # mandantory
693723 EnabledByOwner = EnabledByOwner , HiddenByOwner = HiddenByOwner ,
694- Timestamp = Timestamp )
724+ Timestamp = Timestamp ,
725+ Properties = Properties )
695726 if logger .isEnabledFor (logging .DEBUG ):
696727 logger .debug ("TRACE/" + api_info + ": result=%r" , result )
697728
@@ -747,7 +778,8 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
747778 Permissions = str (Permissions ), # mandatory
748779 EnabledByOwner = EnabledByOwner , HiddenByOwner = HiddenByOwner ,
749780 EnabledByUser = EnabledByUser , HiddenByUser = HiddenByUser ,
750- Timestamp = Timestamp )
781+ Timestamp = Timestamp ,
782+ Properties = Properties )
751783
752784 else :
753785 logger .error (api_info + ": unsupported for ShareType=%r" , ShareType )
@@ -784,8 +816,10 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
784816 EnabledByOwner = EnabledByOwner ,
785817 HiddenByOwner = HiddenByOwner ,
786818 PathOrToken = str (PathOrToken ), # verification above that it is not None
787- Owner = Owner ,
788- Timestamp = Timestamp )
819+ OwnerOrUser = Owner ,
820+ User = User ,
821+ Timestamp = Timestamp ,
822+ Properties = Properties )
789823
790824 elif ShareType == "map" :
791825 result = self .update_sharing (
@@ -795,8 +829,10 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
795829 EnabledByOwner = EnabledByOwner ,
796830 HiddenByOwner = HiddenByOwner ,
797831 PathOrToken = str (PathOrToken ), # verification above that it is not None
798- Owner = Owner ,
799- Timestamp = Timestamp )
832+ OwnerOrUser = Owner ,
833+ User = User ,
834+ Timestamp = Timestamp ,
835+ Properties = Properties )
800836
801837 else :
802838 logger .error (api_info + ": unsupported for ShareType=%r" , ShareType )
@@ -918,18 +954,19 @@ def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: st
918954 answer_array .append (key + '=' + str (answer [key ]))
919955 if 'Content' in answer and answer ['Content' ] is not None :
920956 csv = io .StringIO ()
921- writer = DictWriter (csv , fieldnames = DB_FIELDS_V1 )
957+ writer = DictWriter (csv , fieldnames = DB_FIELDS_V1 , delimiter = ';' )
922958 if output_format == "csv" :
923959 writer .writeheader ()
924960 for entry in answer ['Content' ]:
925- writer .writerow (entry )
961+ # TODO: Argument 1 to "writerow" of "DictWriter" has incompatible type "str"; expected "Mapping[str, Any]" [arg-type]
962+ writer .writerow (entry ) # type: ignore[arg-type]
926963 if output_format == "csv" :
927964 answer_array .append (csv .getvalue ())
928965 else :
929966 index = 0
930967 for line in csv .getvalue ().splitlines ():
931968 # create a shell array with content lines
932- answer_array .append ('Content[' + str (index ) + ']="' + line + '"' )
969+ answer_array .append ('Content[' + str (index ) + ']="' + line . replace ( '"' , ' \\ "' ) + '"' )
933970 index += 1
934971 headers = {
935972 "Content-Type" : "text/csv"
0 commit comments