File Coverage

blib/lib/Net/CardDAVTalk.pm
Criterion Covered Total %
statement 32 249 12.8
branch 0 120 0.0
condition 0 50 0.0
subroutine 11 30 36.6
pod 17 17 100.0
total 60 466 12.8


line stmt bran cond sub pod time code
1             package Net::CardDAVTalk;
2              
3 2     2   66222 use 5.006;
  2         7  
4 2     2   9 use strict;
  2         4  
  2         40  
5 2     2   9 use warnings FATAL => 'all';
  2         3  
  2         52  
6              
7              
8 2     2   521 use Net::DAVTalk;
  2         970491  
  2         63  
9 2     2   15 use base qw(Net::DAVTalk);
  2         5  
  2         157  
10              
11 2     2   12 use Carp;
  2         4  
  2         93  
12 2     2   559 use Text::VCardFast qw(vcard2hash);
  2         4241  
  2         100  
13 2     2   22 use XML::Spice;
  2         3  
  2         13  
14 2     2   65 use URI::Escape qw(uri_unescape);
  2         3  
  2         67  
15 2     2   643 use Net::CardDAVTalk::VCard;
  2         6  
  2         63  
16 2     2   13 use Data::Dumper;
  2         12  
  2         5047  
17              
18              
19             =head1 NAME
20              
21             Net::CardDAVTalk - A library for talking to CardDAV servers
22              
23             =head1 VERSION
24              
25             Version 0.08
26              
27             =cut
28              
29             our $VERSION = '0.08';
30              
31             our $BATCHSIZE = 100;
32              
33              
34             =head1 SYNOPSIS
35              
36             This module maps from CardDAV to an old version of the FastMail API.
37             It's mostly useful as an example of how to talk CardDAV and for the
38             Cyrus IMAP test suite Cassandane.
39              
40             use Net::CardDAVTalk;
41              
42             my $foo = Net::CardDAVTalk->new();
43             ...
44              
45             =head1 SUBROUTINES/METHODS
46              
47             =head2 $class->new()
48              
49             Takes the same arguments as Net::DAVTalk and adds the single
50             namespace:
51              
52             C => 'urn:ietf:params:xml:ns:carddav'
53              
54             =cut
55              
56             sub new {
57 0     0 1   my ($Class, %Params) = @_;
58              
59 0           $Params{homesetns} = 'C';
60 0           $Params{homeset} = 'addressbook-home-set';
61 0           $Params{wellknown} = 'carddav';
62              
63 0           my $Self = $Class->SUPER::new(%Params);
64              
65 0           $Self->ns(C => 'urn:ietf:params:xml:ns:carddav');
66 0           $Self->ns(CY => 'http://cyrusimap.org/ns/');
67              
68 0           return $Self;
69             }
70              
71             # Address book methods {{{
72              
73             =head2 $self->NewAddressBook($Path, %Args)
74              
75             Creates a new addressbook collection. Requires the full
76             path (unlike Net::CalDAVTalk, which creates paths by UUID)
77             and takes a single argument, the name:
78              
79             e.g.
80              
81             $CardDAV->NewAddressBook("Default", name => "Addressbook");
82              
83             =cut
84              
85             sub NewAddressBook {
86 0     0 1   my ($Self, $Path, %Args) = @_;
87              
88 0 0         $Path || confess 'New address book path not specified';
89              
90             $Self->Request(
91             'MKCOL',
92             "$Path/",
93             x('D:mkcol', $Self->NS(),
94             x('D:set',
95             x('D:prop',
96             x('D:resourcetype',
97             x('D:collection'),
98             x('C:addressbook'),
99             ),
100 0           x('D:displayname', $Args{name}),
101             ),
102             ),
103             ),
104             );
105              
106 0           return $Path;
107             }
108              
109             =head2 $self->DeleteAddressBook($Path)
110              
111             Deletes the addressbook at the given path
112              
113             e.g.
114              
115             $CardDAV->DeleteAddressBook("Shared");
116              
117             =cut
118              
119             sub DeleteAddressBook {
120 0     0 1   my ($Self, $Path) = @_;
121              
122 0 0         $Path || confess 'Delete address book path not specified';
123              
124 0           $Self->Request(
125             'DELETE',
126             "$Path/"
127             );
128              
129 0           return 1;
130             }
131              
132             =head2 $self->UpdateAddressBook($Path, %Args)
133              
134             Like 'new', but for an existing addressbook. For now, can only change
135             the name.
136              
137             e.g.
138              
139             $CardDAV->UpdateAddressBook("Default", name => "My Happy Addressbook");
140              
141             =cut
142              
143             sub UpdateAddressBook {
144 0     0 1   my ($Self, $Path, %Args) = @_;
145              
146 0 0         $Path || confess 'Update address book path not specified';
147              
148 0           my @Params;
149              
150 0 0         if (defined $Args{name}) {
151 0           push @Params, x('D:displayname', $Args{name});
152             }
153              
154 0 0         return undef unless @Params;
155              
156 0           $Self->Request(
157             'PROPPATCH',
158             "$Path/",
159             x('D:propertyupdate', $Self->NS(),
160             x('D:set',
161             x('D:prop',
162             @Params,
163             ),
164             ),
165             ),
166             );
167              
168 0           return 1;
169             }
170              
171             =head2 $self->GetAddressBook($Path, %Args)
172              
173             Calls 'GetAddressBooks' with the args, and greps for the one with the
174             matching path.
175              
176             e.g.
177              
178             my $AB = $CardDAV->GetAddressBook("Default");
179              
180             =cut
181              
182             sub GetAddressBook {
183 0     0 1   my ($Self, $Id, %Args) = @_;
184              
185 0           my $Data = $Self->GetAddressBooks(%Args);
186              
187 0 0         die "Can't read data" unless $Data;
188 0           my ($AddressBook) = grep { $_->{path} eq $Id } @$Data;
  0            
189              
190 0           return $AddressBook;
191             }
192              
193             =head2 $self->GetAddressBooks(%Args)
194              
195             Get all the addressbooks on the server.
196              
197             Returns an arrayref of hashrefs
198              
199             e.g.
200              
201             my $ABs = $CardDAV->GetAddressBooks(Sync => 1);
202             foreach my $AB (@$ABs) {
203             say "$AB->{path}: $AB->{name}";
204             }
205              
206             =cut
207              
208             sub GetAddressBooks {
209 0     0 1   my ($Self, %Args) = @_;
210              
211 0   0       my $props = $Args{Properties} || [];
212              
213 0           my $Response = $Self->Request(
214             'PROPFIND',
215             '',
216             x('D:propfind', $Self->NS(),
217             x('D:prop',
218             x('D:displayname'),
219             x('D:resourcetype'),
220             x('D:current-user-privilege-set'),
221             x('D:acl'),
222             x('D:sync-token'),
223             x('D:supported-report-set'),
224             @$props,
225             ),
226             ),
227             Depth => 1,
228             );
229              
230 0           my @AddressBooks;
231              
232 0           my $NS_C = $Self->ns('C');
233 0           my $NS_D = $Self->ns('D');
234 0           my $NS_CY = $Self->ns('CY');
235 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
236             my $HRef = $Response->{"{$NS_D}href"}{content}
237 0   0       || next;
238 0           my $Path = $Self->_unrequest_url($HRef);
239              
240 0 0         foreach my $Propstat (@{$Response->{"{$NS_D}propstat"} || []}) {
  0            
241 0 0         next unless $Propstat->{"{$NS_D}prop"}{"{$NS_D}resourcetype"}{"{$NS_C}addressbook"};
242              
243             # XXX - this is really quite specific and probably wrong-namespaced...
244 0           my $Perms = $Propstat->{"{$NS_D}prop"}{"{$NS_D}current-user-privilege-set"}{"{$NS_D}privilege"};
245              
246 0           my $CanSync;
247 0           my $Report = $Propstat->{"{$NS_D}prop"}{"{$NS_D}supported-report-set"}{"{$NS_D}supported-report"};
248 0 0 0       $Report = [] unless ($Report and ref($Report) eq 'ARRAY');
249 0           foreach my $item (@$Report) {
250             # XXX - do we want to check the other things too?
251 0 0         $CanSync = 1 if $item->{"{$NS_D}report"}{"{$NS_D}sync-collection"};
252             }
253              
254 0           my @ShareWith;
255 0           my $ace = $Propstat->{"{$NS_D}prop"}{"{$NS_D}acl"}{"{$NS_D}ace"};
256 0 0 0       $ace = [] unless ($ace and ref($ace) eq 'ARRAY');
257 0           foreach my $Acl (@$ace) {
258 0 0         next if $Acl->{"{$NS_D}protected"}; # ignore admin ACLs
259 0   0       my $user = uri_unescape($Acl->{"{$NS_D}principal"}{"{$NS_D}href"}{content} // '');
260 0 0         next unless $user =~ m{^/dav/principals/user/([^/]+)};
261 0           my $email = $1;
262 0 0         next if $email eq 'admin';
263 0           my %ShareObject = (
264             email => $email,
265             mayAdmin => $JSON::false,
266             mayWrite => $JSON::false,
267             mayRead => $JSON::false,
268             );
269 0           foreach my $item (@{$Acl->{"{$NS_D}grant"}{"{$NS_D}privilege"}}) {
  0            
270 0 0         $ShareObject{'mayAdmin'} = $JSON::true if $item->{"{$NS_CY}admin"};
271 0 0         $ShareObject{'mayWrite'} = $JSON::true if $item->{"{$NS_D}write-content"};
272 0 0         $ShareObject{'mayRead'} = $JSON::true if $item->{"{$NS_D}read"};
273             }
274              
275 0           push @ShareWith, \%ShareObject;
276             }
277              
278             my %AddressBook = (
279             href => $HRef,
280             path => $Path,
281             name => ($Propstat->{"{$NS_D}prop"}{"{$NS_D}displayname"}{content} || ''),
282 0 0         isReadOnly => (grep { exists $_->{"{$NS_D}write-content"} } @{$Perms || []}) ? $JSON::false : $JSON::true,
  0            
283 0 0         mayRead => (grep { exists $_->{"{$NS_D}read"} } @{$Perms || []}) ? $JSON::true : $JSON::false,
  0            
284 0 0         mayWrite => (grep { exists $_->{"{$NS_D}write-content"} } @{$Perms || []}) ? $JSON::true : $JSON::false,
  0            
285 0 0         mayAdmin => (grep { exists $_->{"{$NS_CY}admin"} } @{$Perms || []}) ? $JSON::true : $JSON::false,
  0            
286             shareWith => (@ShareWith ? \@ShareWith : $JSON::false),
287 0 0 0       syncToken => $Propstat->{"{$NS_D}prop"}{"{$NS_D}sync-token"}{content} || '',
    0 0        
    0          
    0          
    0          
    0          
288             canSync => $CanSync ? $JSON::true : $JSON::false,
289             );
290              
291 0           push @AddressBooks, \%AddressBook;
292             }
293             }
294              
295 0           return \@AddressBooks;
296             }
297              
298             # }}}
299              
300             # Contact methods {{{
301              
302             =head2 $Self->NewContact($AddressBookPath, $VCard)
303              
304             Create a new contact from the Net::CardDAVTalk::VCard object,
305             either using its uid field or generating a new UUID and appending
306             .vcf for the filename.
307              
308             Returns the full path to the card.
309              
310             NOTE: can also be used for a kind: group v4 style group.
311              
312             =cut
313              
314             sub NewContact {
315 0     0 1   my ($Self, $Path, $VCard) = @_;
316              
317 0 0         $Path || confess "New contact path not specified";
318 0 0         $VCard->isa("Net::CardDAVTalk::VCard") || confess "Invalid contact";
319              
320 0   0       my $Uid = $VCard->uid() // $VCard->uid($Self->genuuid());
321              
322 0           $Self->Request(
323             'PUT',
324             "$Path/$Uid.vcf",
325             $VCard->as_string(),
326             'Content-Type' => 'text/vcard',
327             'If-None-Match' => '*',
328             );
329              
330 0           return $VCard->{CPath} = "$Path/$Uid.vcf";
331             }
332              
333             =head2 $self->DeleteContact($Path)
334              
335             Delete the contact at path $Path.
336              
337             =cut
338              
339             sub DeleteContact {
340 0     0 1   my ($Self, $CPath) = @_;
341              
342 0 0         $CPath || confess "Delete contact path not specified";
343              
344 0           $Self->Request(
345             'DELETE',
346             $CPath,
347             );
348              
349 0           return $CPath;
350             }
351              
352             =head2 $Self->UpdateContact($Path, $VCard)
353              
354             Identical to NewContact, but will fail unless there is an
355             existing contact with that path. Also takes the full path
356             instead of just the addressbook path.
357              
358             NOTE: can also be used for a kind: group v4 style group.
359              
360             =cut
361              
362             sub UpdateContact {
363 0     0 1   my ($Self, $CPath, $VCard) = @_;
364              
365 0 0         $CPath || confess "Update contact path not specified";
366 0 0         $VCard->isa("Net::CardDAVTalk::VCard") || confess "Invalid contact";
367              
368 0           $Self->Request(
369             'PUT',
370             $CPath,
371             $VCard->as_string(),
372             'Content-Type' => 'text/vcard',
373             'If-Match' => '*',
374             );
375              
376 0           return $VCard->{CPath} = $CPath;
377             }
378              
379             =head2 $Self->GetContact($Path)
380              
381             Fetch a specific contact by path. Returns a
382             Net::CardDAVTalk::VCard object.
383              
384             =cut
385              
386             sub GetContact {
387 0     0 1   my ($Self, $CPath) = @_;
388              
389 0 0         $CPath || confess "Get contact path not specified";
390              
391 0           my $Response = $Self->Request(
392             'GET',
393             $CPath,
394             );
395              
396             my $Data = $Response && $Response->{content}
397 0   0       // return undef;
      0        
398              
399 0   0       my $VCard = eval { Net::CardDAVTalk::VCard->new_fromstring($Data) }
  0            
400             // return undef;
401              
402 0           $VCard->{CPath} = $CPath;
403              
404 0           return $VCard;
405             }
406              
407             =head2 $Self->GetContactAndProps($Path, $Props)
408              
409             Use a multiget to fetch the properties in the arrayref as well
410             as the card content.
411              
412             Returns the card in scalar context - the card and an array of errors
413             in list context.
414              
415             =cut
416              
417             sub GetContactAndProps {
418 0     0 1   my ($Self, $CPath, $Props) = @_;
419 0   0       $Props //= [];
420              
421 0 0         $CPath || confess "Get contact path not specified";
422              
423             my $Response = $Self->Request(
424             'REPORT',
425             $CPath,
426             x('C:addressbook-multiget', $Self->NS(),
427             x('D:prop',
428             x('D:getetag'),
429             x('D:getcontenttype'),
430             x('C:address-data'),
431 0           map { x(join ":", @$_) } @$Props,
  0            
432             ),
433             x('D:href', $CPath),
434             ),
435             Depth => '0',
436             );
437              
438 0           my ($Contact, @Errors);
439              
440 0           my $NS_C = $Self->ns('C');
441 0           my $NS_D = $Self->ns('D');
442 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
443 0 0         foreach my $Propstat (@{$Response->{"{$NS_D}propstat"} || []}) {
  0            
444 0   0       my $VCard = eval { $Self->_ParseReportData($Response, $Propstat, $Props) } || do {
445             push @Errors, $@ if $@;
446             next;
447             };
448              
449 0           $Contact = $VCard;
450             }
451             }
452              
453 0 0         return wantarray ? ($Contact, \@Errors) : $Contact;
454             }
455              
456             =head2 $self->GetContacts($Path, $Props, %Args)
457              
458             Get multiple cards, possibly including props, using both a propfind
459             AND a multiget.
460              
461             Returns an arrayref of contact and an arrayref of errors (or just the
462             contacts in scalar context again)
463              
464             =cut
465              
466             sub GetContacts {
467 0     0 1   my ($Self, $Path, $Props) = @_;
468              
469 0           my $data = $Self->GetContactLinks($Path);
470 0           my @AllUrls = sort keys %$data;
471              
472 0           my ($Contacts, $Errors, $HRefs) = $Self->GetContactsMulti($Path, \@AllUrls, $Props);
473              
474 0 0         return wantarray ? ($Contacts, $Errors, $HRefs) : $Contacts;
475             }
476              
477             =head2 $self->GetContactLinks($Path)
478              
479             Returns a hash of href => etag for every contact URL (type: text/(x-)?vcard)
480             inside the collection at \$Path.
481              
482             =cut
483              
484             sub GetContactLinks {
485 0     0 1   my ($Self, $Path) = @_;
486              
487 0           my $Response = $Self->Request(
488             'PROPFIND',
489             "$Path/",
490             x('D:propfind', $Self->NS(),
491             x('D:prop',
492             x('D:getcontenttype'),
493             x('D:getetag'),
494             ),
495             ),
496             Depth => '1',
497             );
498              
499 0           my %response;
500 0           my $NS_C = $Self->ns('C');
501 0           my $NS_D = $Self->ns('D');
502 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
503 0           my $href = $Response->{"{$NS_D}href"}{content};
504 0 0         next unless $href;
505 0 0         if ($Response->{"{$NS_D}prop"}{"{$NS_D}getcontenttype"}) {
506 0   0       my $type = $Response->{"{$NS_D}prop"}{"{$NS_D}getcontenttype"}{content} || '';
507 0 0         next unless $type =~ m{text/(x-)?vcard};
508             }
509 0   0       my $etag = $Response->{"{$NS_D}prop"}{"{$NS_D}getetag"}{content} || '';
510 0           $response{$href} = $etag;
511             }
512              
513 0           return \%response;
514             }
515              
516             =head2 $self->GetContactsMulti($Path, $Urls, $Props)
517              
518             Does an addressbook-multiget on the \$Path for all the URLs in \$Urls
519             also fetching \$Props on top of the address-data and getetag.
520              
521             =cut
522              
523             sub GetContactsMulti {
524 0     0 1   my ($Self, $Path, $Urls, $Props) = @_;
525 0   0       $Props //= [];
526 0           my (@Contacts, @Errors, %Links);
527              
528 0           while (my @urls = splice(@$Urls, 0, $BATCHSIZE)) {
529             my $Response = $Self->Request(
530             'REPORT',
531             "$Path/",
532             x('C:addressbook-multiget', $Self->NS(),
533             x('D:prop',
534             x('D:getetag'),
535             x('C:address-data'),
536 0           map { x(join ":", @$_) } @$Props,
537             ),
538 0           map { x('D:href', $_) } @urls,
  0            
539             ),
540             Depth => '0',
541             );
542              
543 0           my $NS_C = $Self->ns('C');
544 0           my $NS_D = $Self->ns('D');
545 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
546 0           my $href = $Response->{"{$NS_D}href"}{content};
547 0 0         next unless $href;
548 0 0         foreach my $Propstat (@{$Response->{"{$NS_D}propstat"} || []}) {
  0            
549 0   0       my $etag = $Propstat->{"{$NS_D}prop"}{"{$NS_D}getetag"}{content} || '';
550 0   0       my $VCard = eval { $Self->_ParseReportData($Response, $Propstat, $Props) } || do {
551             push @Errors, $@ if $@;
552             next;
553             };
554              
555 0           push @Contacts, $VCard;
556              
557 0           $Links{$href} = $etag;
558             }
559             }
560             }
561              
562 0 0         return wantarray ? (\@Contacts, \@Errors, \%Links) : \@Contacts;
563             }
564              
565             =head2 $self->SyncContacts($Path, $Props, %Args)
566              
567             uses the argument 'syncToken' to find newly added and removed
568             cards from the server. Returns just the added/changed contacts
569             in scalar context, or a list of array of contacts, array of
570             removed, array of errors and the new syncToken as 4 items in
571             list context.
572              
573             =cut
574              
575             sub SyncContacts {
576 0     0 1   my ($Self, $Path, $Props, %Args) = @_;
577              
578 0           my ($Added, $Removed, $Errors, $SyncToken) = $Self->SyncContactLinks($Path, %Args);
579              
580 0           my @AllUrls = sort keys %$Added;
581              
582 0           my ($Contacts, $ThisErrors, $Links) = $Self->GetContactsMulti($Path, \@AllUrls, $Props);
583 0           push @$Errors, @$ThisErrors;
584              
585 0 0         return wantarray ? ($Contacts, $Removed, $Errors, $SyncToken, $Links) : $Contacts;
586             }
587              
588             =head2 $self->SyncContactLinks($Path, %Args)
589              
590             uses the argument 'syncToken' to find newly added and removed
591             cards from the server.
592              
593             Returns a list of:
594              
595             * Hash of href to etag for added/changed cargs
596             * List of href of removed cards
597             * List of errors
598             * Scalar value of new syncToken
599              
600             =cut
601              
602             sub SyncContactLinks {
603 0     0 1   my ($Self, $Path, %Args) = @_;
604              
605 0 0         $Path || confess "Sync contacts path required";
606              
607             # WebDAV Collection Synchronization (RFC6578)
608             my $Response = $Self->Request(
609             'REPORT',
610             "$Path/",
611             x('D:sync-collection', $Self->NS(),
612 0 0         x('D:sync-token', ($Args{syncToken} ? ($Args{syncToken}) : ())),
613             x('D:sync-level', 1),
614             x('D:prop',
615             x('D:getetag'),
616             ),
617             ),
618             );
619              
620 0           my (%Added, @Removed, @Errors);
621              
622 0           my $NS_C = $Self->ns('C');
623 0           my $NS_D = $Self->ns('D');
624 0 0         foreach my $Response (@{$Response->{"{$NS_D}response"} || []}) {
  0            
625             my $href = $Response->{"{$NS_D}href"}{content}
626 0   0       || next;
627              
628             # For members that have been removed, the DAV:response MUST
629             # contain one DAV:status with a value set to '404 Not Found' and
630             # MUST NOT contain any DAV:propstat element
631 0 0         if (!$Response->{"{$NS_D}propstat"}) {
632 0           my $Status = $Response->{"{$NS_D}status"}{content};
633 0 0         if ($Status =~ m/ 404 /) {
634 0           push @Removed, $href;
635             } else {
636 0           warn "ODD STATUS";
637 0           push @Errors, "Odd status in non-propstat response $href: $Status";
638             }
639 0           next;
640             }
641              
642             # For members that have changed (i.e., are new or have had their
643             # mapped resource modified), the DAV:response MUST contain at
644             # least one DAV:propstat element and MUST NOT contain any
645             # DAV:status element.
646 0 0         foreach my $Propstat (@{$Response->{"{$NS_D}propstat"} || []}) {
  0            
647 0           my $Status = $Propstat->{"{$NS_D}status"}{content};
648              
649 0 0         if ($Status =~ m/ 200 /) {
    0          
650 0           my $etag = $Propstat->{"{$NS_D}prop"}{"{$NS_D}getetag"}{content};
651 0           $Added{$href} = $etag;
652             }
653             elsif ($Status =~ m/ 404 /) {
654             # Missing properties return 404 status response, ignore
655              
656             }
657             else {
658 0           warn "ODD STATUS";
659 0           push @Errors, "Odd status in propstat response $href: $Status";
660             }
661             }
662             }
663              
664 0           my $SyncToken = $Response->{"{$NS_D}sync-token"}{content};
665              
666 0           return (\%Added, \@Removed, \@Errors, $SyncToken);
667             }
668              
669             =head2 $self->MoveContact($Path, $NewPath)
670              
671             Move a contact to a new path (usually in a new addressbook) - both
672             paths are card paths.
673              
674             =cut
675              
676             sub MoveContact {
677 0     0 1   my ($Self, $CPath, $NewPath) = @_;
678              
679 0 0         $CPath || confess "Move contact path not specified";
680 0 0         $NewPath || confess "Move contact destination path not specified";
681              
682 0           $Self->Request(
683             'MOVE',
684             $CPath,
685             undef,
686             'Destination' => $Self->request_url($NewPath),
687             );
688              
689 0           return $NewPath;
690             }
691              
692             # }}}
693              
694             sub _ParseReportData {
695 0     0     my ($Self, $Response, $Propstat, $Props) = @_;
696              
697 0           my $NS_C = $Self->ns('C');
698 0           my $NS_D = $Self->ns('D');
699              
700             my $HRef = $Response->{"{$NS_D}href"}{content}
701 0   0       // return;
702 0           my $CPath = $Self->_unrequest_url($HRef);
703              
704             my $Data = $Propstat->{"{$NS_D}prop"}{"{$NS_C}address-data"}{content}
705 0   0       // return;
706              
707 0           my $VCard = Net::CardDAVTalk::VCard->new_fromstring($Data);
708 0 0         return unless $VCard;
709              
710 0           $VCard->{CPath} = $CPath;
711 0           $VCard->{href} = $HRef;
712              
713 0           my %Props;
714 0           for (@$Props) {
715 0           my ($NS, $PropName) = @$_;
716 0           my $NS_P = $Self->ns($NS);
717             my $PropValue = $Propstat->{"{$NS_D}prop"}{"{$NS_P}$PropName"}{content}
718 0   0       // next;
719 0           $Props{"${NS}:${PropName}"} = $PropValue;
720             }
721              
722 0           $VCard->{meta} = \%Props;
723              
724 0           return $VCard;
725             }
726              
727             sub _unrequest_url {
728 0     0     my $Self = shift;
729 0           my $Path = shift;
730              
731 0 0         if ($Path =~ m{^/}) {
732 0           $Path =~ s#^\Q$Self->{basepath}\E/?##;
733             } else {
734 0           $Path =~ s#^\Q$Self->{url}\E/?##;
735             }
736 0           $Path =~ s#/$##;
737              
738 0           return $Path;
739             }
740              
741             =head1 AUTHOR
742              
743             Bron Gondwana, C<< <brong at cpan.org> >>
744              
745             =head1 BUGS
746              
747             Please report any bugs or feature requests to C<bug-net-carddavtalk at rt.cpan.org>, or through
748             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Net-CardDAVTalk>. I will be notified, and then you'll
749             automatically be notified of progress on your bug as I make changes.
750              
751              
752              
753              
754             =head1 SUPPORT
755              
756             You can find documentation for this module with the perldoc command.
757              
758             perldoc Net::CardDAVTalk
759              
760              
761             You can also look for information at:
762              
763             =over 4
764              
765             =item * RT: CPAN's request tracker (report bugs here)
766              
767             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Net-CardDAVTalk>
768              
769             =item * AnnoCPAN: Annotated CPAN documentation
770              
771             L<http://annocpan.org/dist/Net-CardDAVTalk>
772              
773             =item * CPAN Ratings
774              
775             L<http://cpanratings.perl.org/d/Net-CardDAVTalk>
776              
777             =item * Search CPAN
778              
779             L<http://search.cpan.org/dist/Net-CardDAVTalk/>
780              
781             =back
782              
783              
784             =head1 ACKNOWLEDGEMENTS
785              
786              
787             =head1 LICENSE AND COPYRIGHT
788              
789             Copyright 2015 FastMail Pty. Ltd.
790              
791             This program is free software; you can redistribute it and/or modify it
792             under the terms of the the Artistic License (2.0). You may obtain a
793             copy of the full license at:
794              
795             L<http://www.perlfoundation.org/artistic_license_2_0>
796              
797             Any use, modification, and distribution of the Standard or Modified
798             Versions is governed by this Artistic License. By using, modifying or
799             distributing the Package, you accept this license. Do not use, modify,
800             or distribute the Package, if you do not accept this license.
801              
802             If your Modified Version has been derived from a Modified Version made
803             by someone other than you, you are nevertheless required to ensure that
804             your Modified Version complies with the requirements of this license.
805              
806             This license does not grant you the right to use any trademark, service
807             mark, tradename, or logo of the Copyright Holder.
808              
809             This license includes the non-exclusive, worldwide, free-of-charge
810             patent license to make, have made, use, offer to sell, sell, import and
811             otherwise transfer the Package with respect to any patent claims
812             licensable by the Copyright Holder that are necessarily infringed by the
813             Package. If you institute patent litigation (including a cross-claim or
814             counterclaim) against any party alleging that the Package constitutes
815             direct or contributory patent infringement, then this Artistic License
816             to you shall terminate on the date that such litigation is filed.
817              
818             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
819             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
820             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
821             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
822             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
823             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
824             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
825             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
826              
827              
828             =cut
829              
830             1; # End of Net::CardDAVTalk