@@ -485,6 +485,235 @@ export default class DavClient {
485485 return this . _request . pathname ( response . body [ '{DAV:}current-user-principal' ] . href )
486486 }
487487
488+ /**
489+ * Creates a CalendarHome instance for an arbitrary calendar home URL.
490+ *
491+ * This can be used to access calendars that are not in the current user's
492+ * own calendar home – for example when acting as a calendar proxy (delegate)
493+ * for another user.
494+ *
495+ * @param {string } calendarHomeUrl Absolute URL of the calendar home
496+ * @return {CalendarHome }
497+ */
498+ getCalendarHomeForUrl ( calendarHomeUrl ) {
499+ const cachedCalendarHome = this . _findCachedCalendarHome ( calendarHomeUrl )
500+ if ( cachedCalendarHome ) {
501+ return cachedCalendarHome
502+ }
503+
504+ const url = this . _request . pathname ( calendarHomeUrl )
505+ return new CalendarHome ( this , this . _request , url , { } )
506+ }
507+
508+ /**
509+ * Finds a previously discovered calendar home by URL.
510+ *
511+ * @param {string } calendarHomeUrl Absolute or relative URL of a calendar home
512+ * @return {CalendarHome|null }
513+ * @private
514+ */
515+ _findCachedCalendarHome ( calendarHomeUrl ) {
516+ const url = this . _request . pathname ( calendarHomeUrl ) . replace ( / \/ ? $ / , '/' )
517+ return this . calendarHomes . find ( ( calendarHome ) => calendarHome . url === url ) || null
518+ }
519+
520+ /**
521+ * Fetches the group-member-set of a principal collection (e.g. a calendar-proxy group).
522+ * @see https://tools.ietf.org/html/rfc3744#section-4.3
523+ *
524+ * @param {string } groupUrl Absolute URL of the proxy group principal
525+ * @return {Promise<string[]> } Absolute URLs of member principals
526+ */
527+ async getGroupMemberSet ( groupUrl ) {
528+ const { body } = await this . _request . propFind ( groupUrl , [
529+ [ NS . DAV , 'group-member-set' ] ,
530+ ] )
531+ const members = body [ `{${ NS . DAV } }group-member-set` ] ?? [ ]
532+ return members . map ( ( href ) => this . _request . absoluteUrl ( href ) )
533+ }
534+
535+ /**
536+ * Sets the group-member-set of a principal collection (e.g. a calendar-proxy group).
537+ * @see https://tools.ietf.org/html/rfc3744#section-4.3
538+ *
539+ * @param {string } groupUrl Absolute URL of the proxy group principal
540+ * @param {string[] } memberUrls Absolute URLs of the new member set
541+ * @return {Promise<void> }
542+ */
543+ async setGroupMemberSet ( groupUrl , memberUrls ) {
544+ const [ skeleton ] = XMLUtility . getRootSkeleton ( [ NS . DAV , 'propertyupdate' ] )
545+ skeleton . children . push ( {
546+ name : [ NS . DAV , 'set' ] ,
547+ children : [ {
548+ name : [ NS . DAV , 'prop' ] ,
549+ children : [ {
550+ name : [ NS . DAV , 'group-member-set' ] ,
551+ children : memberUrls . map ( ( url ) => ( {
552+ name : [ NS . DAV , 'href' ] ,
553+ value : url ,
554+ } ) ) ,
555+ } ] ,
556+ } ] ,
557+ } )
558+ const body = XMLUtility . serialize ( skeleton )
559+ await this . _request . propPatch ( groupUrl , { } , body )
560+ }
561+
562+ /**
563+ * Fetches the group-membership of a principal (the groups it belongs to).
564+ * @see https://tools.ietf.org/html/rfc3744#section-4.4
565+ *
566+ * @param {string } principalUrl Absolute URL of the principal
567+ * @return {Promise<string[]> } Absolute URLs of groups the principal belongs to
568+ */
569+ async getGroupMembership ( principalUrl ) {
570+ const { body } = await this . _request . propFind ( principalUrl , [
571+ [ NS . DAV , 'group-membership' ] ,
572+ ] )
573+ const groups = body [ `{${ NS . DAV } }group-membership` ] ?? [ ]
574+ return groups . map ( ( href ) => this . _request . absoluteUrl ( href ) )
575+ }
576+
577+ /**
578+ * Discovers the calendar home URL for a principal via CalDAV PROPFIND.
579+ *
580+ * Performs a depth-0 PROPFIND on the principal URL requesting the
581+ * calendar-home-set property (RFC 4791 §6.2.1).
582+ *
583+ * @param {string } principalUrl Absolute URL of the principal
584+ * @return {Promise<string|null> } Absolute URL of the calendar home, or null if not found
585+ */
586+ async getCalendarHomeUrlForPrincipal ( principalUrl ) {
587+ const currentPrincipalUrl = this . currentUserPrincipal
588+ ? this . _request . absoluteUrl ( this . currentUserPrincipal . url )
589+ : null
590+ const requestedPrincipalUrl = this . _request . absoluteUrl ( principalUrl )
591+
592+ if ( currentPrincipalUrl === requestedPrincipalUrl && this . calendarHomes . length > 0 ) {
593+ return this . _request . absoluteUrl ( this . calendarHomes [ 0 ] . url )
594+ }
595+
596+ const { body } = await this . _request . propFind ( principalUrl , [
597+ [ NS . IETF_CALDAV , 'calendar-home-set' ] ,
598+ ] )
599+ const homes = body [ `{${ NS . IETF_CALDAV } }calendar-home-set` ]
600+ if ( ! homes || ! homes . length ) {
601+ return null
602+ }
603+ return this . _request . absoluteUrl ( homes [ 0 ] )
604+ }
605+
606+ /**
607+ * Returns the absolute URL of a calendar proxy group for a given principal.
608+ *
609+ * @param {string } principalUrl Absolute URL of the principal
610+ * @param {'write'|'read' } type The proxy group type
611+ * @return {string }
612+ * @private
613+ */
614+ _getProxyGroupUrl ( principalUrl , type ) {
615+ return principalUrl . replace ( / \/ ? $ / , '' ) + '/calendar-proxy-' + type
616+ }
617+
618+ /**
619+ * Returns the absolute URLs of all delegates for a principal, separated by permission level.
620+ *
621+ * @param {string } principalUrl Absolute URL of the principal
622+ * @return {Promise<{write: string[], read: string[]}> } Absolute principal URLs of delegates
623+ */
624+ async getDelegatesForPrincipal ( principalUrl ) {
625+ const [ write , read ] = await Promise . all ( [
626+ this . getGroupMemberSet ( this . _getProxyGroupUrl ( principalUrl , 'write' ) ) ,
627+ this . getGroupMemberSet ( this . _getProxyGroupUrl ( principalUrl , 'read' ) ) ,
628+ ] )
629+ return { write, read }
630+ }
631+
632+ /**
633+ * Adds a delegate to a principal's calendar-proxy group.
634+ *
635+ * Fetches the current member set and appends the new delegate if not already present.
636+ *
637+ * @param {string } ownerPrincipalUrl Absolute URL of the principal who owns the proxy group
638+ * @param {string } delegatePrincipalUrl Absolute or relative URL of the principal to add as delegate
639+ * @param {'write'|'read' } [permission='write'] The proxy group to add the delegate to
640+ * @return {Promise<void> }
641+ */
642+ async addDelegate ( ownerPrincipalUrl , delegatePrincipalUrl , permission = 'write' ) {
643+ const proxyGroupUrl = this . _getProxyGroupUrl ( ownerPrincipalUrl , permission )
644+ const normalizedUrl = this . _request . absoluteUrl ( delegatePrincipalUrl )
645+ const current = await this . getGroupMemberSet ( proxyGroupUrl )
646+ if ( ! current . includes ( normalizedUrl ) ) {
647+ await this . setGroupMemberSet ( proxyGroupUrl , [ ...current , normalizedUrl ] )
648+ }
649+ }
650+
651+ /**
652+ * Removes a delegate from a principal's calendar-proxy group.
653+ *
654+ * @param {string } ownerPrincipalUrl Absolute URL of the principal who owns the proxy group
655+ * @param {string } delegatePrincipalUrl Absolute or relative URL of the principal to remove
656+ * @param {'write'|'read' } [permission='write'] The proxy group to remove the delegate from
657+ * @return {Promise<void> }
658+ */
659+ async removeDelegate ( ownerPrincipalUrl , delegatePrincipalUrl , permission = 'write' ) {
660+ const proxyGroupUrl = this . _getProxyGroupUrl ( ownerPrincipalUrl , permission )
661+ const normalizedUrl = this . _request . absoluteUrl ( delegatePrincipalUrl )
662+ const current = await this . getGroupMemberSet ( proxyGroupUrl )
663+ await this . setGroupMemberSet ( proxyGroupUrl , current . filter ( ( url ) => url !== normalizedUrl ) )
664+ }
665+
666+ /**
667+ * Returns the principal URLs of users who have granted the given principal
668+ * write-proxy (delegate) access.
669+ *
670+ * Inspects the group-membership property for groups ending in
671+ * /calendar-proxy-write and strips that suffix to obtain the owner's
672+ * principal URL.
673+ *
674+ * @param {string } principalUrl Absolute URL of the principal
675+ * @return {Promise<string[]> } Absolute principal URLs of users who delegated to this principal
676+ */
677+ async getDelegatorPrincipalUrls ( principalUrl ) {
678+ const groups = await this . getGroupMembership ( principalUrl )
679+ return groups
680+ . filter ( ( url ) => url . includes ( 'calendar-proxy-write' ) )
681+ . map ( ( url ) => url . replace ( / \/ c a l e n d a r - p r o x y - w r i t e \/ ? $ / , '' ) || null )
682+ . filter ( Boolean )
683+ }
684+
685+ /**
686+ * Returns the principal URLs and permission level of users who have granted
687+ * the given principal proxy access (both read and write).
688+ *
689+ * Inspects the group-membership property for groups ending in
690+ * /calendar-proxy-write or /calendar-proxy-read and returns objects with
691+ * the owner's principal URL and the permission granted.
692+ *
693+ * @param {string } principalUrl Absolute URL of the principal
694+ * @return {Promise<Array<{principalUrl: string, permission: 'write'|'read'}>> }
695+ */
696+ async getDelegatorsWithPermission ( principalUrl ) {
697+ const groups = await this . getGroupMembership ( principalUrl )
698+ const result = [ ]
699+
700+ for ( const groupUrl of groups ) {
701+ if ( groupUrl . includes ( 'calendar-proxy-write' ) ) {
702+ const ownerUrl = groupUrl . replace ( / \/ c a l e n d a r - p r o x y - w r i t e \/ ? $ / , '' )
703+ if ( ownerUrl ) {
704+ result . push ( { principalUrl : ownerUrl , permission : 'write' } )
705+ }
706+ } else if ( groupUrl . includes ( 'calendar-proxy-read' ) ) {
707+ const ownerUrl = groupUrl . replace ( / \/ c a l e n d a r - p r o x y - r e a d \/ ? $ / , '' )
708+ if ( ownerUrl ) {
709+ result . push ( { principalUrl : ownerUrl , permission : 'read' } )
710+ }
711+ }
712+ }
713+
714+ return result
715+ }
716+
488717 /**
489718 * discovers all calendar-homes in this account, all principal collections
490719 * and advertised features
0 commit comments