123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286moduleXml=Webdav_xmlletprop_version=[Xml.pcdata"2"]modulePairMap=Map.Make(structtypet=string*stringletcompare(a1,a2)(b1,b2)=matchString.comparea1b1with|0->String.comparea2b2|x->xend)openSexplib.Convtypeproperty=Xml.attributelist*Xml.treelist[@@derivingsexp]typet=propertyPairMap.ttypeproperty_list=((string*string)*property)list[@@derivingsexp]letto_sexpt=letbindings=PairMap.bindingstinsexp_of_property_listbindingsletof_sexpnows=letbindings=property_list_of_sexpsinletmap=List.fold_left(funmap(k,v)->PairMap.addkvmap)PairMap.emptybindingsinmatchPairMap.find_opt(Xml.robur_ns,"prop_version")mapwith|Some([],[Xml.Pcdatan])->beginmatchint_of_stringnwith|exceptionFailure_->Logs.warn(funm->m"couldn't parse version");map|0|1->letcurrent=[],[Xml.Pcdata(Ptime.to_rfc3339now)]in(* version 0 and 1 didn't write the lastmodified *)(* for directories, we use the current timestamp,
for files the creationdate (which is always updated) *)letts=matchPairMap.find_opt(Xml.dav_ns,"getcontenttype")mapwith|Some([],[Xml.Pcdatact])whenct="text/directory"->current|_->beginmatchPairMap.find_opt(Xml.dav_ns,"creationdate")mapwith|None->Logs.warn(funm->m"map without creationdate");current|Somev->vendinPairMap.add(Xml.robur_ns,"prop_version")([],prop_version)(PairMap.add(Xml.dav_ns,"getlastmodified")tsmap)|_->mapend|_->(* shouldn't happen *)Logs.warn(funm->m"property map without version");map(* not safe *)letunsafe_find=PairMap.find_optletunsafe_add=PairMap.addletunsafe_remove=PairMap.remove(* public and ok *)letempty=PairMap.empty(* internal *)letkeysm=List.mapfst(PairMap.bindingsm)(* public and ok *)letcount=PairMap.cardinalletnot_returned_by_allprop=[(Xml.robur_ns,"prop_version");(Xml.dav_ns,"owner");(Xml.dav_ns,"group");(Xml.dav_ns,"supported-privilege-set");(Xml.dav_ns,"current-user-privilege-set");(Xml.dav_ns,"acl");(Xml.dav_ns,"acl-restrictions");(Xml.dav_ns,"inherited-acl-set");(Xml.dav_ns,"principal-collection-set");(Xml.caldav_ns,"calendar-description");(Xml.caldav_ns,"calendar-timezone");(Xml.caldav_ns,"supported-calendar-component-set");(Xml.caldav_ns,"supported-calendar-data");(Xml.caldav_ns,"max-resource-size");(Xml.caldav_ns,"min-date-time");(Xml.caldav_ns,"max-date-time");(Xml.caldav_ns,"max-instances");(Xml.caldav_ns,"max-attendees-per-instance");(Xml.caldav_ns,"calendar-home-set");(Xml.caldav_ns,"supported-collation-set");(Xml.robur_ns,"password");(Xml.robur_ns,"salt");]letwrite_protected=[(Xml.robur_ns,"prop_version");(Xml.dav_ns,"principal-URL");(Xml.dav_ns,"group-membership");(Xml.dav_ns,"resourcetype");(Xml.dav_ns,"current-user-principal");(Xml.dav_ns,"current-user-privilege-set");(Xml.dav_ns,"content-length");(Xml.dav_ns,"etag");]letcomputed_properties=[(Xml.dav_ns,"current-user-privilege-set");(Xml.dav_ns,"current-user-principal")](* assume that it is safe, should call can_write_prop *)(* TODO check `Write_acl if writing an ACL property *)letpatch?(is_mkcol=false)props_for_resourceupdates=(* if an update did not apply, m will be None! *)letxml(ns,n)=[Xml.node~nsn[]]inletapply(props_for_resource,propstats)update=matchprops_for_resource,updatewith|None,`Set(_,k,_)->None,(`Failed_dependency,xmlk)::propstats|None,`Removek->None,(`Failed_dependency,xmlk)::propstats|Someprops_for_resource',`Set(a,k,v)->ifList.memkwrite_protected&¬(is_mkcol&&k=(Xml.dav_ns,"resourcetype"))thenNone,(`Forbidden,xmlk)::propstatselseletprops_for_resource''=unsafe_addk(a,v)props_for_resource'in(Someprops_for_resource'',(`OK,xmlk)::propstats)|Someprops_for_resource',`Removek->ifList.memkwrite_protectedthenNone,(`Forbidden,xmlk)::propstatselseletprops_for_resource''=unsafe_removekprops_for_resource'inSomeprops_for_resource'',(`OK,xmlk)::propstatsinmatchList.fold_leftapply(Someprops_for_resource,[])updateswith|Someprops_for_resource',xs->Someprops_for_resource',xs|None,xs->(* some update did not apply -> tree: None *)letok_to_failed(s,k)=((matchswith|`OK->`Failed_dependency|x->x),k)inNone,List.mapok_to_failedxs(* housekeeping *)letto_treesm=PairMap.fold(fun(ns,k)(a,v)acc->Xml.node~ns~akv::acc)m[](* housekeeping *)letto_stringm=letc=to_treesminXml.tree_to_string(Xml.dav_node"prop"c)(* housekeeping *)letppppft=Fmt.stringppf@@to_stringt(* housekeeping *)letequalab=String.equal(to_stringa)(to_stringb)(* creates property map for file, only needs to check `Bind in parent, done by webmachine *)letcreate?(initial_props=[])?(content_type="text/html")?(language="en")?(resourcetype=[])acltimestamplengthfilename=letfilename=iffilename=""then"hinz und kunz"elsefilenameinlettimestamp'=Ptime.to_rfc3339timestampinletpropmap=unsafe_add(Xml.robur_ns,"prop_version")([],prop_version)@@unsafe_add(Xml.dav_ns,"acl")([],List.mapXml.ace_to_xmlacl)@@unsafe_add(Xml.dav_ns,"creationdate")([],[Xml.Pcdatatimestamp'])@@unsafe_add(Xml.dav_ns,"displayname")([],[Xml.Pcdatafilename])@@unsafe_add(Xml.dav_ns,"getcontentlanguage")([],[Xml.Pcdatalanguage])@@unsafe_add(Xml.dav_ns,"getcontenttype")([],[Xml.Pcdatacontent_type])@@unsafe_add(Xml.dav_ns,"getcontentlength")([],[Xml.Pcdata(string_of_intlength)])@@unsafe_add(Xml.dav_ns,"getlastmodified")([],[Xml.Pcdatatimestamp'])@@(* unsafe_add "lockdiscovery" *)unsafe_add(Xml.dav_ns,"resourcetype")([],resourcetype)empty(* unsafe_add "supportedlock" *)inList.fold_left(funp(k,v)->unsafe_addkvp)propmapinitial_props(* creates property map for directory *)letcreate_dir?initial_props?(resourcetype=[])acltimestampdirname=create?initial_props~content_type:"text/directory"~resourcetype:(Xml.dav_node"collection"[]::resourcetype)acltimestamp0dirname(* housekeeping *)letfrom_tree=function|Xml.Node(_,"prop",_,children)->List.fold_left(funmc->matchcwith|Xml.Node(ns,k,a,v)->unsafe_add(ns,k)(a,v)m|Xml.Pcdata_->assertfalse)emptychildren|_->assertfalse(* TODO groups only one level deep right now *)(* TODO belongs elsewhere? *)(* outputs identities for a single user *)letidentitiesuserprops=leturl=function|Xml.Node(_,"href",_,[Xml.Pcdataurl])->[Uri.of_stringurl]|_->[]inleturlsn=List.flatten(List.mapurln)inmatchunsafe_find(Xml.dav_ns,"principal-URL")userprops,unsafe_find(Xml.dav_ns,"group-membership")userpropswith|None,_->[]|Some(_,principal),Some(_,groups)->urlsprincipal@urlsgroups|Some(_,principal),None->urlsprincipalletprivileges~auth_user_propsresource_props=letaces=matchunsafe_find(Xml.dav_ns,"acl")resource_propswith|None->[]|Some(_,aces)->acesinPrivileges.list~identities:(identitiesauth_user_props)acesletinherited_acls~auth_user_propsresource_props=letaces=matchunsafe_find(Xml.dav_ns,"acl")resource_propswith|None->[]|Some(_,aces)->acesinLogs.debug(funm->m"inherited aces size %d"(List.lengthaces));letinherited=Privileges.inherited_acls~identities:(identitiesauth_user_props)acesinLogs.debug(funm->m"inherited size %d"(List.lengthinherited));inherited(* helper computing "current-user-privilege-set", not public *)letcurrent_user_privilege_set~auth_user_propsmap=letmake_nodep=Xml.dav_node"privilege"[Xml.priv_to_xmlp]inletprivileges=privileges~auth_user_propsmapinletuniq=(* workaround for Firefox OS which doesn't understand <privilege><all/></privilege> *)ifList.mem`Allprivilegesthen[`Read;`Write;`Read_current_user_privilege_set;`Write_content;`Write_properties;`Bind;`Unbind;`All]elseList.sort_uniqcompareprivilegesinSome([],(List.mapmake_nodeuniq))(* checks nothing, computes current-user-principal, helper function *)letcurrent_user_principalprops=matchunsafe_find(Xml.dav_ns,"principal-URL")propswith|None->Some([],[Xml.dav_node"unauthenticated"[]])|Someurl->Someurl(* checks nothing, computes properties, should be visible? but requires auth_user_props *)letget_propauth_user_propsm=function|ns,"current-user-privilege-set"whenns=Xml.dav_ns->current_user_privilege_set~auth_user_propsm|ns,"current-user-principal"whenns=Xml.dav_ns->current_user_principalauth_user_props|fqname->unsafe_findfqnamemletfind~auth_user_props~resource_propsproperty_fqname=letprivileges=privileges~auth_user_propsresource_propsinifPrivileges.can_read_propproperty_fqnameprivilegesthenmatchget_propauth_user_propsresource_propsproperty_fqnamewith|None->Error`Not_found|Somev->OkvelseError`Forbiddenlettransform_lastmodified=function|None->None|Some(attrs,[Xml.Pcdatastr])->Some(attrs,[Xml.Pcdata(Xml.rfc3339_date_to_http_datestr)])|Some_->assertfalse(* checks sufficient privileges for "current-user-privilege-set" and "read-acl" via can_read_prop *)letfind_many~auth_user_props~resource_propsproperty_names=letresource_props=PairMap.update(Xml.dav_ns,"getlastmodified")transform_lastmodifiedresource_propsinletprops=List.map(find~auth_user_props~resource_props)property_namesinletresults=List.map2(fun(ns,name)p->p,matchpwith|Ok(a,c)->Xml.node~ns~anamec|Error_->Xml.node~nsname[])property_namespropsin(* group by return code *)letfound,rest=List.partition(function|Ok_,_->true|_->false)resultsinletnot_found,forbidden=List.partition(function|Error`Not_found,_->true|Error`Forbidden,_->false|Ok_,_->assertfalse)restinletapply_tagtagl=ifl=[]then[]else[tag,List.mapsndl]inapply_tag`OKfound@apply_tag`Not_foundnot_found@apply_tag`Forbiddenforbidden(* not safe, exposed, returns property names *)letnamesm=List.map(fun(ns,k)->Xml.node~nsk[])@@computed_properties@keysm(* not really safe, but excludes from the not-returned-by-allprop list *)letallm=letm'=PairMap.update(Xml.dav_ns,"getlastmodified")transform_lastmodifiedminto_trees(List.fold_rightunsafe_removenot_returned_by_allpropm')